blob: 950f4a680c46548260fadbd216689ab08f09cacd [file] [log] [blame]
# Copyright 2022 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Recipe for building EDK II UEFI firmware for QEMU"""
from collections import namedtuple
DEPS = [
"fuchsia/buildbucket_util",
"fuchsia/cas_util",
"fuchsia/cipd_util",
"fuchsia/git",
"fuchsia/git_checkout",
"fuchsia/python3",
"recipe_engine/cipd",
"recipe_engine/context",
"recipe_engine/file",
"recipe_engine/path",
"recipe_engine/platform",
"recipe_engine/step",
]
# Bump this number whenever changing this recipe in ways that affect the
# package built without also changing any upstream revision pin.
RECIPE_SALT = ""
EDK2_REPO = "https://fuchsia.googlesource.com/third_party/edk2"
EDK2_REF = "refs/tags/edk2-stable202308"
LIBUUID_REPO = "https://kernel.googlesource.com/pub/scm/utils/util-linux/util-linux"
LIBUUID_REF = "refs/tags/v2.39.2"
ACPICA_REPO = "https://fuchsia.googlesource.com/third_party/acpica"
ACPICA_REF = "refs/heads/upstream/master"
#
# Build flags, as they are spelled by the project.
#
# TODO(fxbug.dev/99325): Unfortunately, there are too many bugs and obstacles
# to getting the firmware built with clang at the moment; try again down the
# road.
TOOLCHAIN_TAGNAME = "GCC"
BUILDTARGET = "RELEASE"
# Ranges from 0 (lowest) to 9 (highest). In the event of failures, it is
# recommended to debug with a value of 9; this ensures failure messages that
# are actually useful (e.g., python backtraces).
DEBUG_LEVEL = 0
# Represents an EDK II "package", the basic development unit. More or less
# corresponds to a top-level directory in edk2.git.
Package = namedtuple(
"Package",
[
# (str): The name of the package. Just used for display purposes.
"name",
# (str): The target architecture for the package, as spelled by the
# project (e.g., "X64" or "AARCH64").
"arch",
# (Path): Points to the "platform" file (*.dsc), detailing the
# firmware to build.
"platform",
# (str): The top-level subdirectory of the build directory under which
# the corresponding firmware images will be found.
"build_subdir",
# (str): The top-level directory of the final uploaded directory under
# which the corresponding firmware images will be organized.
"staging_subdir",
# (list(SizeConstraint)): List of size constraints on the associated
# firmware volumes.
"size_constraints",
],
)
SizeConstraint = namedtuple(
"SizeConstraint",
[
# (str): The path to a firmware volume within a package artifact
# directory.
"volume",
# (int): The size in mebibytes that the firmware volume should be extended
# until.
"size_mb",
],
)
def RunSteps(api):
checkout_dir, revision = api.git_checkout(
EDK2_REPO, fallback_ref=EDK2_REF, submodules=True, recursive=True
)
# TODO(joshuaseaton): Report or fix bugs upstream where possible.
with api.context(cwd=checkout_dir):
api.git.apply_patchfile(api.resource("ld-notext.patch"))
api.git.apply_patchfile(api.resource("no-maybe-uninitialized.patch"))
# NOTE: Ideally we would use the GCCNOLTO toolchain, but there is
# no RISCV64 version of that yet.
api.git.apply_patchfile(api.resource("no-lto.patch"))
cipd_dir = api.path.start_dir / "cipd"
with api.step.nest("ensure packages"):
with api.context(infra_steps=True):
pkgs = api.cipd.EnsureFile()
pkgs.add_package(
"fuchsia/third_party/sysroot/linux", "integration", "sysroot"
)
pkgs.add_package("fuchsia/third_party/clang/${platform}", "integration")
pkgs.add_package("fuchsia/third_party/gcc/${platform}", "integration")
# autopoint is needed to build libuuid.
pkgs.add_package(
"fuchsia/third_party/source/gettext",
"version:0.19.8.1",
"source/gettext",
)
#
# The usual GNU tools for building gettext and libuuid.
#
pkgs.add_package("fuchsia/third_party/autoconf/${platform}", "version:2.69")
pkgs.add_package(
"fuchsia/third_party/automake/${platform}", "version:1.16.2"
)
pkgs.add_package("fuchsia/third_party/bison/${platform}", "version:3.7")
pkgs.add_package("fuchsia/third_party/libtool/${platform}", "version:2.4.6")
pkgs.add_package("fuchsia/third_party/m4/${platform}", "version:1.4.18")
pkgs.add_package("fuchsia/third_party/make/${platform}", "version:4.3")
pkgs.add_package(
"fuchsia/third_party/pkg-config/${platform}", "version:0.29.2"
)
# Used in the firmware builds.
pkgs.add_package(
"fuchsia/third_party/iasl/${platform}", "version:2@2020.09.25", "bin"
)
pkgs.add_package(
"fuchsia/third_party/nasm/${platform}", "version:2@2.15.05.1"
)
api.cipd.ensure(cipd_dir, pkgs)
bin_dir = cipd_dir / "bin"
sysroot = cipd_dir / "sysroot"
#
# We need to build BaseTools before building the firmware.
#
pkg_dir = api.path.start_dir / "pkgconfig"
pkg_bin_dir = pkg_dir / "bin"
api.file.ensure_directory("create pkg directory", pkg_dir)
with api.context(
env={
"PKG_CONFIG_PATH": "",
"PKG_CONFIG_SYSROOT_DIR": sysroot,
"PKG_CONFIG_ALLOW_SYSTEM_CFLAGS": 0,
"PKG_CONFIG_ALLOW_SYSTEM_LIBS": 0,
"PKG_CONFIG_LIBDIR": ":".join(
[
str(pkg_dir.joinpath("share", "pkgconfig")),
str(pkg_dir.joinpath("lib", "pkgconfig")),
str(pkg_dir.joinpath("lib64", "pkgconfig")),
]
),
# TODO(fxbug.dev/58251): remove environment variable when possible.
"M4": bin_dir / "m4",
},
env_prefixes={"PATH": [bin_dir, pkg_bin_dir]},
):
# The BaseTools build requires libuuid, and the uuid build requires
# autopoint.
BuildGettext(api, cipd_dir / "source/gettext", pkg_dir, sysroot)
BuildUuid(api, pkg_dir, sysroot)
base_tools_dir = checkout_dir / "BaseTools"
optflags = [
f"--sysroot={sysroot}",
f"-I{pkg_dir / 'include'}",
"-Wno-deprecated-non-prototype",
]
ldflags = [
f"--sysroot={sysroot}",
f"-L{pkg_dir / 'lib'}",
]
env = Environment(sysroot)
env.update(
{
"EXTRA_OPTFLAGS": " ".join(optflags),
"EXTRA_LDFLAGS": " ".join(ldflags),
"PYTHON_COMMAND": "vpython3",
}
)
with api.context(cwd=base_tools_dir, env=env):
api.step("build BaseTools", ["make"])
# https://edk2-docs.gitbook.io/edk-ii-build-specification/4_edk_ii_build_process_overview/43_pre-build_stage_overview
#
# This copy happens as part of a `source edksetup.sh`, which is not
# recipe-kosher - so we copy them manually.
for basename in ["build_rule", "tools_def", "target"]:
src = base_tools_dir.joinpath("Conf", basename + ".template")
dest = checkout_dir.joinpath("Conf", basename + ".txt")
api.file.copy(f"install {api.path.basename(dest)}", src, dest)
with api.context(
cwd=checkout_dir,
env={
"WORKSPACE": checkout_dir,
"EDK_TOOLS_PATH": base_tools_dir,
"PYTHON_COMMAND": "vpython3",
# Annoyingly, GCC_AARCH64_PREFIX is an environment variable that
# can be set by the user, but GCC_X64_PREFIX is not and is defined
# in terms of a GCC_BIN one.
"GCC_AARCH64_PREFIX": "aarch64-elf-",
"GCC_BIN": "x86_64-elf-",
},
env_prefixes={
"PATH": [
base_tools_dir.joinpath("BinWrappers", "PosixLike"),
bin_dir,
pkg_bin_dir,
],
},
):
staging_dir = api.path.mkdtemp("staging")
build_dir = checkout_dir / "Build"
for pkg in Packages(checkout_dir):
with api.step.nest(f"build {pkg.name}"):
api.step(
"build",
[
"build",
"--verbose",
f"--debug={int(DEBUG_LEVEL)}",
f"--tagname={TOOLCHAIN_TAGNAME}",
f"--buildtarget={BUILDTARGET}",
f"--arch={pkg.arch}",
f"--platform={pkg.platform}",
"-n",
api.platform.cpu_count,
"--define=NETWORK_IP6_ENABLE",
"--define=NETWORK_HTTP_BOOT_ENABLE",
],
)
# Where the corresponding firmware volume images are found.
fv_dir = build_dir.joinpath(
pkg.build_subdir,
f"{BUILDTARGET}_{TOOLCHAIN_TAGNAME}",
"FV",
)
dest_dir = staging_dir / pkg.staging_subdir
api.file.copytree(
"stage",
fv_dir,
dest_dir,
)
for constraint in pkg.size_constraints:
# Sadly, we cannot just use api.file.truncate() directly,
# as that overwrites the provided file with a new one.
api.python3(
f"resize {pkg.staging_subdir}/{constraint.volume}",
[
api.resource("resize.py"),
"--file",
dest_dir / constraint.volume,
"--size-mb",
constraint.size_mb,
],
)
api.file.copy(
"stage License.txt",
checkout_dir / "License.txt",
staging_dir / "License.txt",
)
# Unconditionally upload to CAS; this gives a simple view into the layout
# of what might eventually be uploaded to CIPD.
api.cas_util.upload(staging_dir, output_property="isolated")
if not api.buildbucket_util.is_dev_or_try:
api.cipd_util.upload_package(
"fuchsia/third_party/edk2",
staging_dir,
search_tag={
"git_revision": revision + (f",{RECIPE_SALT}" if RECIPE_SALT else "")
},
repository=EDK2_REPO,
)
def Packages(checkout_dir):
return [
Package(
name="OvmfPkg",
arch="X64",
platform=checkout_dir.joinpath("OvmfPkg", "OvmfPkgX64.dsc"),
build_subdir="OvmfX64",
staging_subdir="qemu-x64",
size_constraints=[],
),
Package(
name="ArmVirtPkg",
arch="AARCH64",
platform=checkout_dir.joinpath("ArmVirtPkg", "ArmVirtQemu.dsc"),
build_subdir="ArmVirtQemu-AARCH64",
staging_subdir="qemu-arm64",
# Undocumented "virt" machine restriction that these volumes should
# be precisely 64MiB to be stored in emulated flash.
size_constraints=[
SizeConstraint(
volume="QEMU_EFI.fd",
size_mb=64,
),
SizeConstraint(
volume="QEMU_VARS.fd",
size_mb=64,
),
],
),
]
def BuildGettext(api, src_dir, pkg_dir, sysroot):
with api.step.nest("build gettext"):
ConfigureAndMake(
api,
src_dir / "gettext-tools",
pkg_dir,
sysroot,
configure_flags=[
"--disable-curses",
"--disable-dependency-tracking",
"--disable-silent-rules",
"--disable-debug",
"--disable-shared",
"--disable-java",
"--disable-csharp",
"--disable-c++",
"--disable-openmp",
"--enable-static",
"--with-pic",
"--with-included-gettext",
"--with-included-glib",
"--with-included-libcroco",
"--with-included-libunistring",
"--with-included-libxml",
"--without-git",
"--without-cvs",
"--without-xz",
],
ldflags=["-lm"],
)
def BuildUuid(api, pkg_dir, sysroot):
with api.step.nest("build uuid"):
src_dir, _ = api.git_checkout(
LIBUUID_REPO,
fallback_ref=LIBUUID_REF,
submodules=True,
recursive=True,
)
with api.context(cwd=src_dir):
api.step("autogen", ["./autogen.sh"])
ConfigureAndMake(
api,
src_dir,
pkg_dir,
sysroot,
configure_flags=[
"--disable-shared",
"--enable-static",
"--disable-all-programs",
"--enable-libuuid",
"--enable-libuuid-force-uuidd",
],
)
# Invokes the configure script for the given source and installs it in the
# given package directory.
#
# Assumes that any needed tool is already available in $PATH.
#
# The configure flag `--prefix` is set by this function (to the package
# directory) and should not be supplied.
def ConfigureAndMake(
api,
src_dir,
pkg_dir,
sysroot,
configure_flags,
ldflags=[],
):
with api.context(cwd=pkg_dir):
try:
api.step(
"configure",
[
src_dir / "configure",
f"--prefix={pkg_dir}",
]
+ configure_flags
+ sorted(
[f"{k}={v}" for k, v in Environment(sysroot, ldflags).items()]
),
)
finally:
api.file.read_text(
"config.log",
pkg_dir / "config.log",
test_data="error",
)
api.step("make", ["make", f"-j{int(api.platform.cpu_count)}"])
api.step("make install", ["make", "install"])
def Environment(sysroot, cflags=[], ldflags=[]):
extra_flags = [f"--sysroot={sysroot}"]
return {
"CC": "clang",
"CXX": "clang++",
"CFLAGS": " ".join(cflags + extra_flags),
"CPPFLAGS": " ".join(cflags + extra_flags),
"CXXFLAGS": " ".join(extra_flags),
"LD": "clang",
"LDFLAGS": " ".join(extra_flags + ldflags),
"AR": "llvm-ar",
"RANLIB": "llvm-ranlib",
"NM": "llvm-nm",
"STRIP": "llvm-strip",
"OBJCOPY": "llvm-objcopy",
}
def GenTests(api):
yield api.buildbucket_util.test("ci")
yield api.buildbucket_util.test("cq", tryjob=True)