From 58af029f787adae64abf401f6be3d779abc6669a Mon Sep 17 00:00:00 2001 From: Robert Drab <robert.drab@huawei.com> Date: Tue, 3 Aug 2021 10:16:36 +0200 Subject: [PATCH] gn.bbclass: add class for building GN-based projects GN meta-build system uses it's own meta-language to describe build process of the project including toolchain definition, compiler flags, dependencies, etc. GN tool parses the meta data and generates ninja file for the final build process. For the project to use Yocto-provided toolchain and flags it is necessary to patch the BUILDCONFIG.gn file in following manner: - change set_default_toolchain function call to use //build/toolchain/yocto:yocto_target - append [ "//build/toolchain/yocto:yocto_flags" ] to configs variable in all set_defaults function calls Closes: https://git.ostc-eu.org/OSTC/OHOS/meta-ohos/-/issues/64 Closes: https://git.ostc-eu.org/OSTC/OHOS/meta-ohos/-/issues/65 Closes: https://git.ostc-eu.org/OSTC/OHOS/meta-ohos/-/issues/66 Signed-off-by: Robert Drab <robert.drab@huawei.com> --- meta-ohos-staging/classes/gn.bbclass | 307 +++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 meta-ohos-staging/classes/gn.bbclass diff --git a/meta-ohos-staging/classes/gn.bbclass b/meta-ohos-staging/classes/gn.bbclass new file mode 100644 index 00000000..f533f062 --- /dev/null +++ b/meta-ohos-staging/classes/gn.bbclass @@ -0,0 +1,307 @@ +# SPDX-FileCopyrightText: Huawei Inc. +# +# SPDX-License-Identifier: MIT + +# This class allows building GN-based projects. +# GN toolchain definition file for Yocto toolchain is generated using +# write_toolchain_file function originating from the meta-browser layer. This +# Yocto toolchain definition relies on the toolchain template of a given +# project. GN_TOOLCHAIN_TMPL_FILE variable defines the location of toolchain +# template in a GN label syntax. Generated Yocto toolchain has to be set as +# default toolchain by patching project's BUILDCONFIG.gn file. Also special +# yocto_flags config containing all Yocto toolchain flags is generated. This +# config have to be added to default configs of all targets using set_defaults +# function. For more information please refer do_check_yocto_toolchain_is_used +# task below. Lastly, recipe has to provide do_install task. + +DEPENDS += "gn-native ninja-native" + +# Location of the GN toolchain template file, used in write_toolchain_file(). It +# can be overridden in the recipe as needed. The name of the GN toolchain +# template does not matter and so by default gcc_toolchain template is used even +# for clang compiler +GN_TOOLCHAIN_TMPL_FILE ??= "//build/toolchain/gcc_toolchain.gni" + +# Project's GN toolchains labels that should be used by the generated Yocto +# toolchain definition - can be overridden in the recipe +GN_TARGET_TOOLCHAIN_TMPL_LABEL ??= "gcc_toolchain" +GN_HOST_TOOLCHAIN_TMPL_LABEL ??= "gcc_toolchain" + +# General GN options, like --dotfile +GN_OPTIONS ??= "" + +# GN_ARGS can be added in the recipe +GN_ARGS ?= ' \ + target_cpu="${@gn_target_arch_name(d)}" \ +' + +B = "${WORKDIR}/out" + +gn_do_configure() { + cd ${S} + gn gen ${GN_OPTIONS} --args='${GN_ARGS}' -C ${B} +} + +gn_do_compile() { + ninja -C ${B} +} + +gn_do_install() { + bbfatal " \ + Missing do_install task definition! \ + GN projects don't usually follow any particular convention with regards \ + to build artifacts, therefore do_install task has to be defined in the \ + project's recipe. \ + " +} + +# GN fails with unclear and confusing error logs when build directory is the +# same as source directory. To avoid that build directory is set to +# "S{WORKDIR}/out". Nevertheless let's make sure that B != S as devtool default +# behaviour is to override B variable to be the same as S. +# +# NOTE: devtool adds bbappend file which makes recipe to inherit externalsrc +# class with EXTERNALSRC set to workspace/sources/<recipe-name> and by default +# EXTERNALSRC_BUILD set to the same value as EXTERNALSRC. To change this +# behavior --no-same-dir option has to be passed to devtool add command. +python do_check_B_is_not_S() { + bpath = os.path.abspath(d.expand("${B}")) + spath = os.path.abspath(d.expand("${S}")) + if os.path.abspath(d.expand("${S}")) == os.path.abspath(d.expand("${B}")): + bb.fatal(''' +GN requires build and sources directories to be different. By default build +directory is set to ${WORKDIR}/out. If you're using devtool remember to use +--no-same-dir option, e.g.: +devtool add --no-same-dir <your-gn-project-name> <your-gn-project-git-url> + ''') +} + +addtask check_B_is_not_S after do_patch before do_configure + +python do_write_gn_toolchain_file () { + root_gn_dir = d.expand("${S}") + # Do not modify the below part: for simplicity hardcoded GN references: + # //build/toolchain/yocto:yocto_{target,native,flags} are used in a couple + # of places including this class implementation and patchwork for projects + # using this class + toolchain_dir = os.path.join(root_gn_dir, "build", "toolchain", "yocto") + bb.utils.mkdirhier(toolchain_dir) + toolchain_file = os.path.join(toolchain_dir, "BUILD.gn") + write_toolchain_file(d, toolchain_file) +} + +addtask write_gn_toolchain_file after do_patch before do_configure + +do_check_yocto_toolchain_is_used() { + cd "${S}" + DEFAULT_TARGET_TOOLCHAIN=$(gn desc ${B} "//build/toolchain/yocto:yocto_flags" | \ + grep "toolchain: //build/toolchain/yocto:yocto_target") + LIST_OF_GN_TARGETS_USING_YOCTO_FLAGS=$(gn refs -q ${B} \ + "//build/toolchain/yocto:yocto_flags") + if test -z "$DEFAULT_TARGET_TOOLCHAIN" || \ + test -z "$LIST_OF_GN_TARGETS_USING_YOCTO_FLAGS" + then + bbfatal "\ +No reference to yocto_target or yocto_flags! \ +You have to patch project's default toolchain and targets defaults! Those are \ +found in the BUILDCONFIG.gn file. Toolchain is set by the set_default_toolchain \ +function call and targets defaults by a series of calls to set_defaults \ +function one per target type, like executable, shared_library, etc. Typical \ +approach is to change default toolchain to (sample patch): \n\ +\n\ +--- a/build/config/BUILDCONFIG.gn\n\ ++++ b/build/config/BUILDCONFIG.gn\n\ +- set_default_toolchain(_default_toolchain)\n\ ++ set_default_toolchain(\"//build/toolchain/yocto:yocto_target\")\n\ +\n\ +and append flags to all defaults' config (sample patch): \n\ +\n\ +--- a/build/config/BUILDCONFIG.gn\n\ ++++ b/build/config/BUILDCONFIG.gn\n\ +set_defaults(\"shared_library\") {\n\ +- configs = default_configs\n\ ++ configs =\n\ ++ default_configs +\n\ ++ [ \"//build/toolchain/yocto:yocto_flags\" ]\n\ +}\n\ +\n\ + " + fi +} + +addtask do_check_yocto_toolchain_is_used after do_configure before do_compile + +EXPORT_FUNCTIONS do_configure do_compile do_install + +def is_clang(cc: str) -> bool: + """ Returns True when the argument (cc) string contains the word `clang`; + False otherwise""" + cc_basename: str = cc.split()[0] + return "clang" in cc_basename + +def gn_bool(arg: bool) -> str: + """ Returns GN-compliant lowercase boolean string from argument (arg) """ + return str(arg).lower() + +def gn_list(space_separated_str: str) -> str: + """ Returns GN-formatted list string: + ["item1", "item2"] + string extracted from a space-separated string argument + (space_separated_str) """ + return str(space_separated_str.split()).replace("'", "\"") + +# Following functions originally come from the meta-browser layer: +# https://github.com/OSSystems/meta-browser + +# GN host architecture helpers. +# +# BUILD_ARCH's value corresponds to what uname returns as the machine name. +# The mapping in gn_host_arch_name() tries to match several possible values +# returned by the Linux kernel in uname(2) into the corresponding values GN +# understands. +def gn_host_arch_name(d): + """Returns a GN architecture name corresponding to the build host's machine + architecture.""" + import re + arch_translations = { + r'aarch64.*': 'arm64', + r'arm.*': 'arm', + r'i[3456]86$': 'x86', + r'x86_64$': 'x64', + r'riscv32$': 'riscv32', + r'riscv64$': 'riscv64', + } + build_arch = d.getVar("BUILD_ARCH") + for arch_regexp, gn_arch_name in arch_translations.items(): + if re.match(arch_regexp, build_arch): + return gn_arch_name + bb.fatal('Unsuported BUILD_ARCH value: "%s"' % build_arch) + +# GN target architecture helpers. +# +# Determining the target architecture is more difficult, as there are many +# different values we can use on the Yocto side (e.g. TUNE_ARCH, TARGET_ARCH, +# MACHINEOVERRIDES etc). What we do is define the mapping with regular, +# non-Python variables with overrides that are generic enough (i.e. "x86" +# instead of "i586") and then use gn_target_arch_name() to return the right +# value with some validation. +GN_TARGET_ARCH_NAME_aarch64 = "arm64" +GN_TARGET_ARCH_NAME_arm = "arm" +GN_TARGET_ARCH_NAME_x86 = "x86" +GN_TARGET_ARCH_NAME_x86-64 = "x64" +GN_TARGET_ARCH_NAME_riscv32 = "riscv32" +GN_TARGET_ARCH_NAME_riscv64 = "riscv64" + +def gn_target_arch_name(d): + """Returns a GN architecture name corresponding to the target machine's + architecture.""" + name = d.getVar("GN_TARGET_ARCH_NAME") + if name is None: + bb.fatal('Unsupported target architecture. A valid override for the ' + 'GN_TARGET_ARCH_NAME variable could not be found.') + return name + +def gn_toolchain_file_header(d): + """Reurns GN toolchain file header as a multi-line string""" + + toolchain_tmpl_file = d.expand('${GN_TOOLCHAIN_TMPL_FILE}') + return str( + '# This file has been generated automatically.\n' + '\n' + f'import("{toolchain_tmpl_file}")\n' + '\n' + ) + +def gn_toolchain_flags(d): + """Returns GN config containing Yocto flags as a multi-line string""" + + yocto_target_cflags = gn_list(d.expand('${TARGET_CPPFLAGS} ${TARGET_CFLAGS}')) + yocto_target_cflags_cc = gn_list(d.expand('${TARGET_CXXFLAGS}')) + yocto_target_ldflags = gn_list(d.expand('${TARGET_LDFLAGS}')) + yocto_native_cflags = gn_list(d.expand('${BUILD_CPPFLAGS} ${BUILD_CFLAGS}')) + yocto_native_cflags_cc = gn_list(d.expand('${BUILD_CXXFLAGS}')) + yocto_native_ldflags = gn_list(d.expand('${BUILD_LDFLAGS}')) + + return str( + 'config("yocto_flags") {\n' + ' if (current_toolchain == "//build/toolchain/yocto:yocto_target") {\n' + f' cflags = {yocto_target_cflags}\n' + f' cflags_cc = {yocto_target_cflags_cc}\n' + f' ldflags = {yocto_target_ldflags}\n' + ' } else if (current_toolchain == "//build/toolchain/yocto:yocto_native") {\n' + f' cflags = {yocto_native_cflags}\n' + f' cflags_cc = {yocto_native_cflags_cc}\n' + f' ldflags = {yocto_native_ldflags}\n' + ' }\n' + '}\n' + '\n' + ) + +def gn_toolchain(toolchain_args): + """Returns GN toolchain definition based on provided (toolchain_args) + dictionary variable as a multi-line string""" + + # Using template to avoid having to escape curly braces in str.format + from string import Template + return Template( + '${toolchain_tmpl_name}("${toolchain_name}") {\n' + ' cc = "${cc}"\n' + ' cxx = "${cxx}"\n' + ' ar = "${ar}"\n' + ' ld = cxx # GN expects a compiler, not a linker.\n' + ' nm = "${nm}"\n' + ' readelf = "${readelf}"\n' + ' toolchain_args = {\n' + ' current_cpu = "${current_cpu}"\n' + ' current_os = "linux"\n' + ' is_clang = ${is_clang}\n' + ' }\n' + ' # TODO: In some projects not all of the above variables are used\n' + ' # Marking all of those as "not_needed" to avoid errors\n' + ' # This should be fixed in some smart way if feasible\n' + ' not_needed("*")\n' + '}\n' + ).substitute(toolchain_args) + +def gn_toolchain_native(d): + """Returns GN toolchain definition based on Yocto native toolchain""" + + native_toolchain_args = { + 'toolchain_tmpl_name': d.expand('${GN_HOST_TOOLCHAIN_TMPL_LABEL}'), + 'toolchain_name': 'yocto_native', + 'current_cpu': gn_host_arch_name(d), + 'cc': d.expand('${BUILD_CC}'), + 'cxx': d.expand('${BUILD_CXX}'), + 'ar': d.expand('${BUILD_AR}'), + 'nm': d.expand('${BUILD_NM}'), + 'readelf': d.expand('${BUILD_PREFIX}readelf'), + 'is_clang': gn_bool(is_clang(d.expand('${BUILD_CC}'))) + } + + return gn_toolchain(native_toolchain_args) + +def gn_toolchain_target(d): + """Returns GN toolchain definition based on Yocto target toolchain""" + + target_toolchain_args = { + 'toolchain_tmpl_name': d.expand('${GN_TARGET_TOOLCHAIN_TMPL_LABEL}'), + 'toolchain_name': 'yocto_target', + 'current_cpu': gn_target_arch_name(d), + 'cc': d.expand('${CC}'), + 'cxx': d.expand('${CXX}'), + 'ar': d.expand('${AR}'), + 'nm': d.expand('${NM}'), + 'readelf': d.expand('${TARGET_PREFIX}readelf'), + 'is_clang': gn_bool(is_clang(d.expand('${CC}'))) + } + + return gn_toolchain(target_toolchain_args) + +def write_toolchain_file(d, file_path): + """Creates a complete GN toolchain file in |file_path|.""" + + with open(file_path, 'w') as toolchain_file: + toolchain_file.write(gn_toolchain_file_header(d)) + toolchain_file.write(gn_toolchain_flags(d)) + toolchain_file.write(gn_toolchain_native(d)) + toolchain_file.write(gn_toolchain_target(d)) -- GitLab