308028fb创建于 4月20日历史提交
#!/usr/bin/env python3
#
# Copyright (c) 2010 - 2026, Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# We kindly request you to use one or more of the following phrases to refer to
# foxBMS in your hardware, software, documentation or advertising materials:
#
# - "This product uses parts of foxBMS®"
# - "This product includes parts of foxBMS®"
# - "This product is derived from foxBMS®"

# This script defines how to configure and build the project.
# This includes configuration the toolchain for building foxBMS binaries, the
# documentation and running various checks on the source files.


import argparse
import os
import shutil
import sys
import tarfile
from binascii import hexlify

from waflib import Build, Context, Errors, Logs, Scripting, Utils
from waflib.Build import (
    BuildContext,
    CleanContext,
    ListContext,
    StepContext,
)
from waflib.ConfigSet import ConfigSet
from waflib.Configure import ConfigurationContext

Context.Context.line_just = 50

out = "build"
top = "."

APPNAME = "foxBMS"

# foxBMS version; this is included in the embedded binaries, as well as the
# documentation and fox.py
VERSION = "1.11.0"

# Single source of truth for variant metadata.
# - 'cat' controls command-context generation.
# - 'dir' is recursed in build().
# - 'doc' displayed when using '--help'.
# - 'env' is selected als build environment in build().
VARIANT_CONFIGS = {
    "app_doxygen": {
        "dir": "docs",
        "doc": "doxygen documentation for the app",
        "env": "doxygen",
    },
    "app_doxygen_unit_test": {
        "dir": "docs",
        "doc": "doxygen documentation for the app's unit tests",
        "env": "doxygen",
    },
    "app_embedded": {
        "cat": "binary",
        "dir": "src",
        "doc": "embedded binary of the app",
        "env": "",
    },
    "bootloader_doxygen": {
        "dir": "docs",
        "doc": "doxygen documentation for the bootloader",
        "env": "doxygen",
    },
    "bootloader_doxygen_unit_test": {
        "dir": "docs",
        "doc": "doxygen documentation for the bootloader's unit tests",
        "env": "doxygen",
    },
    "bootloader_embedded": {
        "cat": "binary",
        "dir": "src",
        "doc": "embedded binary of the bootloader",
        "env": "",
    },
    "docs": {
        "dir": "docs",
        "doc": "general documentation",
        "env": "docs",
    },
}

TOOLDIR = "tools/waf-tools"

BMS_CONFIG = {
    "bms": "conf/bms/bms.json",
}


for var, var_cfg in VARIANT_CONFIGS.items():
    contexts: tuple = (BuildContext, CleanContext)
    if var_cfg.get("cat") in ("binary",):
        contexts += (ListContext, StepContext)
    old_contexts = contexts
    for cont in contexts:
        name = cont.__name__.replace("Context", "").lower()

        class tmp_1(cont):
            """Helper class to create the build variant commands"""

            if name == "build":
                __doc__ = f"builds the {var_cfg.get('doc', var)}"
            elif name == "install":
                __doc__ = f"installs the {var_cfg.get('doc', var)}"
            elif name == "clean":
                __doc__ = f"cleans the {var_cfg.get('doc', var)}"
            elif name == "list":
                __doc__ = f"lists the {var_cfg.get('doc', var)}"
            elif name == "step":
                __doc__ = f"steps the {var_cfg.get('doc', var)}"
            cmd = str(name) + "_" + var
            variant = var


def options(opt):
    opt.load("sphinx", tooldir=TOOLDIR)
    opt.load("doxygen", tooldir=TOOLDIR)
    opt.load("f_ti_arm_cgt", tooldir=TOOLDIR)
    # load bootstrap-library-project-tool
    opt.load("f_bootstrap_library_project", tooldir=TOOLDIR)
    opt.load("all_commands", tooldir=TOOLDIR)

    # remove options hard
    for k in (
        "--out",
        "--top",
        "--prefix",
        "--destdir",
        "--bindir",
        "--libdir",
        "--msvc_version",
        "--msvc_targets",
        "--no-msvc-lazy",
        "--force",
        "--check-c-compiler",
    ):
        option = opt.parser.get_option(k)
        if option:
            opt.parser.remove_option(k)

    Context.classes.remove(Build.InstallContext)
    Context.classes.remove(Build.UninstallContext)

    opt.add_option(
        "--confcache",
        dest="confcache",
        default=0,
        action="count",
        help="Use a configuration cache",
    )

    opt.load("f_lauterbach", tooldir=TOOLDIR)

    # test function that shall not be communicate through the CLI
    opt.add_option(
        "--target-test",
        dest="target_test",
        default=False,
        action="store_true",
    )
    opt.parser.get_option("--target-test").help = argparse.SUPPRESS


# pylint: disable-next=too-many-statements
def configure(conf: ConfigurationContext):
    # This basic configuration shall be loaded as initial step to every
    # environment that is created
    conf.env.APPNAME = APPNAME
    conf.env.VERSION = VERSION
    bms_config_node = conf.path.find_node(BMS_CONFIG["bms"])
    conf.env.append_unique(
        "CONFIG_BMS_JSON_HASH", hexlify(bms_config_node.h_file()).decode("utf-8")
    )
    conf.load("check_project_path", tooldir=TOOLDIR)
    # Save the default environment; all things that shall be common to all
    # environments shall be added above this line!
    default_env_node = conf.path.find_or_declare("default.env")
    env_copy = conf.env.derive()
    env_copy.store(default_env_node.abspath())
    # Basic environment creation done

    conf.load("version_validator", tooldir=TOOLDIR)
    conf.version_consistency_checker()

    # ENV: environment for sphinx documentation builds
    conf.setenv("docs")
    conf.env = ConfigSet()
    conf.env.load(default_env_node.abspath())
    conf.load("sphinx_build", tooldir=TOOLDIR)
    conf.env.detach()

    # ENV: environment for doxygen documentation builds
    conf.setenv("doxygen")
    conf.env = ConfigSet()
    conf.env.load(default_env_node.abspath())
    conf.load("doxygen", tooldir=TOOLDIR)
    conf.env.detach()

    # ENV: environment for target and SPA builds
    conf.setenv("")
    conf.env = ConfigSet()
    conf.env.load(default_env_node.abspath())
    # We have a basic setup, now we can check what features are available
    conf.find_program("python", var="PYTHON")
    conf.load("vcs_git", tooldir=TOOLDIR)
    conf.load("f_ti_arm_cgt", tooldir=TOOLDIR)
    # test code fragments, that shall work with all compilers
    snippet_main = conf.path.find_node("conf/cc/snippet_main.c").read()
    snippet_sum = conf.path.find_node("conf/cc/snippet_sum.c").read()
    msg = "Checking for code snippet (object)"
    conf.check(features="c", fragment=snippet_main, msg=msg)
    msg = "Checking for code snippet (library)"
    conf.check(features="c cstlib", fragment=snippet_sum, msg=msg)

    def full_build(bld):
        bld.env.APPNAME = "TEST_BUILD"
        c_fragment = "#include <stdint.h>\n\nint main() {\n    return 0;\n}\n"
        h_fragment = (
            "#ifndef GENERAL_H_\n#define GENERAL_H_\n#include <stdbool.h>\n"
            "#include <stdint.h>\n#endif /* GENERAL_H_ */\n"
        )
        source = bld.srcnode.make_node("test.c")
        source.parent.mkdir()
        source.write(c_fragment, encoding="utf-8")
        include = bld.srcnode.make_node("general.h")
        include.write(h_fragment, encoding="utf-8")
        linker_script = bld.path.find_node("../../src/app/main/app.cmd")
        version_header = bld.path.find_node("../../src/version/version.h")
        cflags = []
        if bld.env.RTSV_missing:
            cflags = ["--diag_remark=10366"]
        bld.tiprogram(
            includes=[include.parent, version_header.parent],
            source=[source],
            cflags=cflags,
            linker_script=linker_script,
        )

    default_env = conf.env
    test_env = conf.env.derive()
    test_env.detach()

    conf.setenv("test_env", test_env)
    rtsv_lib = "rtsv7R4_A_be_v3D16_eabi.lib"
    base_dir = f"{conf.root.find_node(conf.env.get_flat('CC')).parent.parent.abspath()}"
    rtsv_lib_path = conf.root.find_node(f"{base_dir}/lib/{rtsv_lib}")
    if not rtsv_lib_path.exists():
        Logs.warn(
            f"Runtime support library '{rtsv_lib}' missing. Need to build "
            "it first. The next step may take a while..."
        )
        conf.env.RTSV_missing = True
    else:
        conf.env.RTSV_missing = False
    conf.env.STLIB = ["c"]
    conf.env.TARGETLIB = []
    if "--undef_sym=resetEntry" in conf.env.LINKFLAGS:
        conf.env.LINKFLAGS.remove("--undef_sym=resetEntry")
    try:
        conf.check(msg="Checking for code snippet (program)", build_fun=full_build)
    except conf.errors.ConfigurationError:
        Logs.error("\n\n===============>>>>>")
        Logs.error(f"===============>>>>> Missing permissions for '{rtsv_lib_path}'. ")
        Logs.error(
            "===============>>>>> The RTS library exists, but the current "
            "users permissions are not sufficient."
        )
        Logs.error(
            "===============>>>>> Increase the current users permissions "
            "on this file and then try again."
        )
        Logs.error("===============>>>>>\n\n")
        conf.fatal("Exit.")

    conf.setenv("", default_env)

    conf.load("f_bootstrap_library_project", tooldir=TOOLDIR)

    # configure the documentation toolchain
    conf.load("f_lauterbach", tooldir=TOOLDIR)

    # Configure the build for the correct RTOS
    conf.load("bms_config_validator", tooldir=TOOLDIR)
    conf.validate_bms_configuration(bms_config_node)
    conf.load("codegen_matlab", tooldir=TOOLDIR)

    # load VS Code setup as last foxBMS specific tool to ensure that all
    # variables have a meaningful value
    conf.load("f_vscode", tooldir=TOOLDIR)
    conf.load("hash_check", tooldir=TOOLDIR)

    # configuration is done, write the config header
    config_header_name = "foxbms_config.h"
    conf.write_config_header(config_header_name)
    config_header = conf.path.find_resource(config_header_name)
    conf.env.append_unique("INCLUDES", [config_header.parent.abspath()])
    conf.env.append_unique("CFLAGS_HAL", ["--preinclude", config_header.abspath()])
    conf.env.append_unique("CFLAGS_OS", ["--preinclude", config_header.abspath()])
    conf.msg(msg="Configuration header", result=config_header.abspath())
    # NOTHING BEYOND THIS POINT MUST BE DONE IN THE CONFIGURATION STEP


def build(bld: BuildContext):
    variant_config = VARIANT_CONFIGS.get(bld.variant)
    if not variant_config:
        bld.fatal(
            f"Build variants are:\n - {'\n - '.join(VARIANT_CONFIGS.keys())}.\n"
            f"For more details use '--help'."
        )

    env_name = variant_config.get("env", "")
    bld.env = bld.all_envs[env_name]
    bld.version_consistency_checker()
    bld.env.append_unique(
        "CMD_FILES", [bld.path.find_node("conf/cc/remarks.txt").abspath()]
    )
    if not bld.env.CONFIG_BMS_JSON_HASH[0] == hexlify(
        bld.path.find_node(BMS_CONFIG["bms"]).h_file()
    ).decode("utf-8"):
        bld.fatal(f"{BMS_CONFIG} has changed. Please run the configure command again.")

    bld.recurse(variant_config["dir"])


Scripting.Dist.base_name = APPNAME.lower()
Scripting.Dist.algo = "tar.gz"
Scripting.Dist.excl = DIST_EXCLUDE = (
    f"{out}/** "
    f"{APPNAME.lower()}/** "
    ".vs* "
    "**/.git "
    "**/.gitignore "
    ".gitlab/** "
    "**/.gitattributes "
    "**/*.tar.bz2 "
    "**/*.tar.gz "
    "**/*.pyc "
    "**/*.pyo "
    "tests/hil/** "
    "tools/waf*.*.**-* "
    ".lock-* "
)


class DistCheckFoxBMS(Scripting.DistCheck):
    def make_distcheck_cmd(self: Scripting.DistCheck, tmpdir: str = ""):  # noqa:ARG002
        dist_waf = os.path.relpath(sys.argv[0], self.path.abspath())
        cmd = [
            sys.executable,
            os.path.join(self.path.abspath(), self.get_base_name(), dist_waf),
            "configure",
            "build_all",
        ]
        return cmd

    def make_distcheck_cmd_additional(self: Scripting.DistCheck):
        """Additional commands that need to be checked in the distribution,
        which are not waf-based commands
        """
        cmd = (
            [
                sys.executable,
                os.path.join(self.path.abspath(), self.get_base_name(), "fox.py"),
                "ceedling",
                "--project",
                "app",
                "gcov:all",
            ],
            [
                sys.executable,
                os.path.join(self.path.abspath(), self.get_base_name(), "fox.py"),
                "ceedling",
                "--project",
                "bootloader",
                "gcov:all",
            ],
        )
        return cmd

    def check(self):
        with tarfile.open(self.get_arch_name()) as t:
            for x in t:
                if hasattr(tarfile, "data_filter"):
                    t.extract(x, filter="data")
                else:
                    t.extract(x)

        for cmd in (
            self.make_distcheck_cmd(""),
        ) + self.make_distcheck_cmd_additional():
            ret = Utils.subprocess.Popen(cmd, cwd=self.get_base_name()).wait()
            if ret:
                err = f"distcheck failed with code {ret}"
                raise Errors.WafError(err)

        shutil.rmtree(self.get_base_name())