diff options
-rw-r--r-- | zephyr/CMakeLists.txt | 30 | ||||
-rw-r--r-- | zephyr/zmake/tests/test_util.py | 15 | ||||
-rw-r--r-- | zephyr/zmake/tests/test_version.py | 81 | ||||
-rw-r--r-- | zephyr/zmake/tests/test_zmake.py | 3 | ||||
-rw-r--r-- | zephyr/zmake/zmake/util.py | 23 | ||||
-rw-r--r-- | zephyr/zmake/zmake/version.py | 43 | ||||
-rw-r--r-- | zephyr/zmake/zmake/zmake.py | 12 |
7 files changed, 180 insertions, 27 deletions
diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index af577cdc34..171ab973d2 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -25,33 +25,11 @@ if(NOT EXISTS "${PLATFORM_EC}/zephyr/module.yml") assert_exists("${PLATFORM_EC}/zephyr/module.yml") endif() -if(DEFINED CONFIG_PLATFORM_EC) - # Build the EC's expected ec_version.h file. - set(ec_version_file "${CMAKE_BINARY_DIR}/ec/include/generated/ec_version.h") - # Create the command used to generate the ec_version.h file. - add_custom_command( - OUTPUT ${ec_version_file} - # TODO(b/185249526): zephyr: add project name to the version output - COMMAND BOARD=${BOARD}_zephyr ${PLATFORM_EC}/util/getversion.sh > ${ec_version_file} - WORKING_DIRECTORY ${PLATFORM_EC} - ) - # Create a custom target that will depend on the file. - add_custom_target(generate_ec_version DEPENDS ${ec_version_file}) - - # Create a local library (ec_version) that will be a pure interface library - # and will depend on the custom target that generates the ec_version.h file. - add_library(ec_version INTERFACE) - add_dependencies(ec_version generate_ec_version) - - # Ensure the EC version file is generated before trying to build the app - # library, which includes compiling version.c - add_dependencies(app ec_version) - - # Register the library with zephyr so that it generates the file at build - # time. Also, append the include directory so we can include ec_version.h. - zephyr_append_cmake_library(ec_version) - zephyr_include_directories("${CMAKE_BINARY_DIR}/ec/include/generated") +if(DEFINED ZMAKE_INCLUDE_DIR) + zephyr_include_directories("${ZMAKE_INCLUDE_DIR}") +endif() +if(DEFINED CONFIG_PLATFORM_EC) # Add CHROMIUM_EC definition, which is used by ec_commands.h to # determine that the header is being compiled for the EC instead of # by another third-party C codebase. diff --git a/zephyr/zmake/tests/test_util.py b/zephyr/zmake/tests/test_util.py index 15f92f58ab..0524a153fd 100644 --- a/zephyr/zmake/tests/test_util.py +++ b/zephyr/zmake/tests/test_util.py @@ -87,3 +87,18 @@ def test_read_kconfig_autoconf_value(value): f.write("#define TEST {}".format(value)) read_value = util.read_kconfig_autoconf_value(path, "TEST") assert int(read_value) == value + + +@pytest.mark.parametrize( + ["input_str", "expected_result"], + [ + ("", '""'), + ("TROGDOR ABC-123", '"TROGDOR ABC-123"'), + ("hello world", '"hello world"'), + ("hello\nworld", r'"hello\nworld"'), + ('hello"world', r'"hello\"world"'), + ("hello\\world", '"hello\\\\world"'), + ], +) +def test_c_str(input_str, expected_result): + assert util.c_str(input_str) == expected_result diff --git a/zephyr/zmake/tests/test_version.py b/zephyr/zmake/tests/test_version.py index ff8e366736..8fb4a09435 100644 --- a/zephyr/zmake/tests/test_version.py +++ b/zephyr/zmake/tests/test_version.py @@ -2,7 +2,11 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import datetime import subprocess +import unittest.mock as mock + +import pytest import zmake.project import zmake.version as version @@ -96,3 +100,80 @@ def test_version_string_static(tmp_path): version.get_version_string(project, zephyr_base, modules, static=True) == "prj_v2.6.0-STATIC" ) + + +@pytest.fixture +def fake_user_hostname(): + with mock.patch("getpass.getuser", return_value="toukmond", autospec=True): + with mock.patch("platform.node", return_value="pokey", autospec=True): + yield + + +@pytest.fixture +def fake_date(): + fixed_date = datetime.datetime(2021, 6, 28, 3, 18, 53) + with mock.patch("datetime.datetime") as mock_datetime: + mock_datetime.now.return_value = fixed_date + yield + + +HEADER_VERSION_STR = "trogdor_v2.6.1004-cmsis:0dead0,hal_stm32:0beef0,os:ad00da" +EXPECTED_HEADER = ( + "/* This file is automatically generated by zmake */\n" + '#define VERSION "trogdor_v2.6.1004-cmsis:0dead0,hal_stm32:0beef0,os:ad00da"\n' + '#define CROS_EC_VERSION32 "trogdor_v2.6.1004-cmsis:0dead0,"\n' + '#define BUILDER "toukmond@pokey"\n' + '#define DATE "2021-06-28 03:18:53"\n' +) +HEADER_VERSION_STR_STATIC = "trogdor_v2.6.0-STATIC" +EXPECTED_HEADER_STATIC = ( + "/* This file is automatically generated by zmake */\n" + '#define VERSION "trogdor_v2.6.0-STATIC"\n' + '#define CROS_EC_VERSION32 "trogdor_v2.6.0-STATIC"\n' + '#define BUILDER "reproducible@build"\n' + '#define DATE "STATIC_VERSION_DATE"\n' +) + + +def test_header_gen(fake_user_hostname, fake_date, tmp_path): + # Test the simple case (static=False, no existing header). + output_file = tmp_path / "ec_version.h" + version.write_version_header(HEADER_VERSION_STR, output_file) + assert output_file.read_text() == EXPECTED_HEADER + + +def test_header_gen_reproducible_build(tmp_path): + # With static=True this time. + output_file = tmp_path / "ec_version.h" + version.write_version_header(HEADER_VERSION_STR_STATIC, output_file, static=True) + assert output_file.read_text() == EXPECTED_HEADER_STATIC + + +def test_header_gen_exists_not_changed(fake_user_hostname, fake_date, tmp_path): + # Test we don't overwrite if no changes needed. + output_file = tmp_path / "ec_version.h" + + # First time, write and record mtime. + version.write_version_header(HEADER_VERSION_STR, output_file) + expected_mtime = output_file.stat().st_mtime + + # Do another write (contents should be unchanged). + version.write_version_header(HEADER_VERSION_STR, output_file) + + # Assert we didn't write again. + assert output_file.stat().st_mtime == expected_mtime + + +def test_header_gen_exists_needs_changes(fake_user_hostname, fake_date, tmp_path): + # Test we overwrite when it exists already and changes are needed. + output_file = tmp_path / "ec_version.h" + + # First time, write and save contents. + version.write_version_header(HEADER_VERSION_STR, output_file) + original_contents = output_file.read_text() + + # Do another write (contents should be changed). + version.write_version_header(HEADER_VERSION_STR_STATIC, output_file, static=True) + + # Assert we overwrote. + assert output_file.read_text() != original_contents diff --git a/zephyr/zmake/tests/test_zmake.py b/zephyr/zmake/tests/test_zmake.py index a5c132dbaa..f7978764f8 100644 --- a/zephyr/zmake/tests/test_zmake.py +++ b/zephyr/zmake/tests/test_zmake.py @@ -141,7 +141,8 @@ EXTRAVERSION = pathlib.Path(tmpname), build_dir=pathlib.Path("build") ) else: - zmk.build(pathlib.Path(tmpname)) + with patch("zmake.version.write_version_header", autospec=True): + zmk.build(pathlib.Path(tmpname)) multiproc.wait_for_log_end() recs = [rec.getMessage() for rec in cap.records] diff --git a/zephyr/zmake/zmake/util.py b/zephyr/zmake/zmake/util.py index 18d03dd7f0..455cb7c9d6 100644 --- a/zephyr/zmake/zmake/util.py +++ b/zephyr/zmake/zmake/util.py @@ -9,6 +9,29 @@ import re import shlex +def c_str(input_str): + """Make a string that can be included as a literal in C source code. + + Args: + input_str: The string to process. + + Returns: + A string which can be included in C source code. + """ + + def c_chr(char): + # Convert a char in a string to the C representation. Per the + # C standard, we can use all characters but quote, newline, + # and backslash directly with no replacements. + return { + '"': r"\"", + "\n": r"\n", + "\\": "\\\\", + }.get(char, char) + + return '"{}"'.format("".join(map(c_chr, input_str))) + + def locate_cros_checkout(): """Find the path to the ChromiumOS checkout. diff --git a/zephyr/zmake/zmake/version.py b/zephyr/zmake/zmake/version.py index 404c05cb74..2d505769f2 100644 --- a/zephyr/zmake/zmake/version.py +++ b/zephyr/zmake/zmake/version.py @@ -2,7 +2,11 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import datetime +import getpass +import io import os +import platform import subprocess import zmake.util as util @@ -114,3 +118,42 @@ def get_version_string(project, zephyr_base, modules, static=False): return "{}_v{}.{}.{}-{}".format( project_id, major_version, minor_version, num_commits, vcs_hashes ) + + +def write_version_header(version_str, output_path, static=False): + """Generate a version header and write it to the specified path. + + Generate a version header in the format expected by the EC build + system, and write it out only if the version header does not exist + or changes. We don't write in the case that the version header + does exist and was unchanged, which allows "zmake build" commands + on an unchanged tree to be an effective no-op. + + Args: + version_str: The version string to be used in the header, such + as one generated by get_version_string. + output_path: The file path to write at (a pathlib.Path + object). + static: If true, generate a header which does not include + information like the username, hostname, or date, allowing + the build to be reproducible. + """ + output = io.StringIO() + output.write("/* This file is automatically generated by zmake */\n") + + def add_def(name, value): + output.write("#define {} {}\n".format(name, util.c_str(value))) + + add_def("VERSION", version_str) + add_def("CROS_EC_VERSION32", version_str[:31]) + + if static: + add_def("BUILDER", "reproducible@build") + add_def("DATE", "STATIC_VERSION_DATE") + else: + add_def("BUILDER", "{}@{}".format(getpass.getuser(), platform.node())) + add_def("DATE", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + + contents = output.getvalue() + if not output_path.exists() or output_path.read_text() != contents: + output_path.write_text(contents) diff --git a/zephyr/zmake/zmake/zmake.py b/zephyr/zmake/zmake/zmake.py index 45e23fd904..c6ec3879ea 100644 --- a/zephyr/zmake/zmake/zmake.py +++ b/zephyr/zmake/zmake/zmake.py @@ -235,6 +235,7 @@ class Zmake: self.logger.info("Clearing old build directory %s", build_dir) shutil.rmtree(build_dir) + generated_include_dir = (build_dir / "include").resolve() base_config = zmake.build_config.BuildConfig( environ_defs={"ZEPHYR_BASE": str(zephyr_base), "PATH": "/usr/bin"}, cmake_defs={ @@ -242,6 +243,7 @@ class Zmake: "SYSCALL_INCLUDE_DIRS": str( self.module_paths["ec"] / "zephyr" / "include" / "drivers" ), + "ZMAKE_INCLUDE_DIR": str(generated_include_dir), }, ) @@ -274,6 +276,8 @@ class Zmake: if not build_dir.exists(): build_dir = build_dir.mkdir() + if not generated_include_dir.exists(): + generated_include_dir.mkdir() processes = [] self.logger.info("Building %s in %s.", project_dir, build_dir) for build_name, build_config in project.iter_builds(): @@ -375,6 +379,14 @@ class Zmake: zmake.modules.locate_from_directory(build_dir / "modules"), ) + # The version header needs to generated during the build phase + # instead of configure, as the tree may have changed since + # configure was run. + zmake.version.write_version_header( + version_string, + build_dir / "include" / "ec_version.h", + ) + for build_name, build_config in project.iter_builds(): with self.jobserver.get_job(): dirs[build_name] = build_dir / "build-{}".format(build_name) |