From abbf856cce5470f419f028a231de3e196f5256ec Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 14 Oct 2026 07:07:54 +0000 Subject: [PATCH 1/8] Separate the deployed platform from the showcases that verify it Image content is now split into three layers: the image itself (//images/), the deployed S-CORE platform (//deploy) and the showcases that demonstrate and verify it (//showcases). Before this change the two upper layers were mixed in both directions: the lifecycle showcase shipped the launch_manager and the standalone showcase shipped the config_daemon, while the persistency test scenarios binary was packaged with the platform applications. A showcase that ships the component it exercises verifies the binary it brought along instead of the one the distribution deployed, so a distribution failing to deploy an S-CORE module would still pass. - Add //deploy with one package per module (lifecycle, logging, communication, config_management) and the aggregate :platform_bundle / :platform_tar that every image consumes. - Move launch_manager and config_daemon from the showcases into //deploy; the showcase manifests now drive the deployed binaries. - Move the persistency test scenarios binary from the platform packaging into //showcases. - Fold images/common/apps into //deploy and point all images at the single platform bundle. - Document the layering and the layer 2 / layer 3 contract in deploy/README.md and in the platform integration guide. --- deploy/BUILD | 43 ++++++++++++ deploy/README.md | 57 +++++++++++++++ deploy/communication/BUILD | 32 +++++++++ deploy/config_management/BUILD | 45 ++++++++++++ deploy/lifecycle/BUILD | 32 +++++++++ deploy/logging/BUILD | 39 +++++++++++ docs/integration_process/step_4_platforms.rst | 69 ++++++++++++------- images/common/apps/BUILD | 68 ------------------ images/linux_x86_64/BUILD | 12 +--- images/qnx_aarch64/build/BUILD | 12 ++-- images/qnx_x86_64/build/BUILD | 10 +-- showcases/BUILD | 2 + showcases/simple_lifecycle/BUILD | 4 +- .../simple_lifecycle.score.json | 2 +- showcases/standalone/BUILD | 22 ++++-- .../standalone/config_management.score.json | 6 +- 16 files changed, 326 insertions(+), 129 deletions(-) create mode 100644 deploy/BUILD create mode 100644 deploy/README.md create mode 100644 deploy/communication/BUILD create mode 100644 deploy/config_management/BUILD create mode 100644 deploy/lifecycle/BUILD create mode 100644 deploy/logging/BUILD delete mode 100644 images/common/apps/BUILD diff --git a/deploy/BUILD b/deploy/BUILD new file mode 100644 index 00000000000..0a02b93d4af --- /dev/null +++ b/deploy/BUILD @@ -0,0 +1,43 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_filegroup") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") + +exports_files(["README.md"]) + +# The S-CORE platform as deployed by this distribution: every binary and +# configuration file that an S-CORE module contributes to the shipped image. +# This is the system under test for the conformance showcases; it must stay +# free of anything that only exists to demonstrate or test the platform. +# +# Consumed by the QNX images via qnx_ifs srcs. +pkg_filegroup( + name = "platform_bundle", + srcs = [ + "//deploy/communication:etc_comm_configs", + "//deploy/config_management:config_daemon_bin", + "//deploy/config_management:config_daemon_configs", + "//deploy/lifecycle:launch_manager_bin", + "//deploy/logging:datarouter_bin", + "//deploy/logging:datarouter_configs", + ], + visibility = ["//visibility:public"], +) + +# Same content as :platform_bundle, as a tar for oci_image consumers. +pkg_tar( + name = "platform_tar", + srcs = [":platform_bundle"], + visibility = ["//visibility:public"], +) diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000000..e7710771e1b --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,57 @@ +# Platform deployment (layer 2) + +The image content of this distribution is split into three layers: + +| Layer | Location | Content | +| --- | --- | --- | +| 1 — Image | `//images/` | Boot, kernel, drivers, root filesystem skeleton | +| 2 — Platform | `//deploy` | The S-CORE modules as deployed — the system under test | +| 3 — Showcases | `//showcases` | Applications that demonstrate and verify the platform | + +A production image ships layers 1 and 2. Layer 3 must be removable without +changing either of them. + +## Why layer 2 and layer 3 are separate + +The showcases are the probe, the platform is the probe's subject. If a +showcase shipped the component it exercises — as the lifecycle showcase did +with the launch manager before this split — then the check would be verifying +the binary it brought along rather than the one the distribution deployed. A +distribution that fails to deploy an S-CORE module would still pass. + +Keeping them apart gives the property that makes the checks meaningful: the +showcase applications run against whatever platform the image provides, and +fail when it is missing. + +That is also why a module's binaries can end up in different layers. The +lifecycle launch manager is deployed here; the applications it supervises, +and the launch manager configuration that declares them, live in +`//showcases/simple_lifecycle`. + +## Contract between layer 2 and layer 3 + +Layer 3 may rely on the following, and on nothing else: + +| Component | Deployed location | +| --- | --- | +| `launch_manager` | `/usr/bin/launch_manager` | +| `datarouter` | `/usr/bin/datarouter/datarouter`, config in `/usr/bin/datarouter/etc` | +| `config_daemon` | `/usr/bin/config_management/config_daemon`, config in `/usr/bin/config_management/etc` | +| mw::com system configuration | `/etc/mw_com_config.json` | + +Layer 2 never references anything under `/showcases`. The one place where +layer 2 reads layer 3 data is the launch manager, which is started by the +showcase with a working directory that holds the showcase's own +configuration — the configuration is passed in, not deployed alongside the +daemon. + +## Adding a module + +Add a `deploy//BUILD` that packages the module's binaries and +configuration with `pkg_files`, then list those targets in +`//deploy:platform_bundle`. Keep examples, demo applications and test +binaries out; those belong in `//showcases`. + +Long term each S-CORE module should export its own deployment package +upstream, so that "deployed correctly" becomes checkable rather than agreed. +Until then this directory is the distribution's local stand-in for it. diff --git a/deploy/communication/BUILD b/deploy/communication/BUILD new file mode 100644 index 00000000000..8d15117d677 --- /dev/null +++ b/deploy/communication/BUILD @@ -0,0 +1,32 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Deployment of the S-CORE communication module (layer 2, see //deploy:README.md). +# +# Communication ships as a library, so the only deployed artifacts are the +# system-wide configuration files every mw::com participant reads from /etc. + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") + +package(default_visibility = ["//deploy:__pkg__"]) + +pkg_files( + name = "etc_comm_configs", + srcs = [ + "//feature_integration_tests/configs:etc/logging.json", + "//feature_integration_tests/configs:etc/mw_com_config.json", + ], + attributes = pkg_attributes(mode = "0777"), + prefix = "etc", + strip_prefix = strip_prefix.files_only(), +) diff --git a/deploy/config_management/BUILD b/deploy/config_management/BUILD new file mode 100644 index 00000000000..e3ad78712b1 --- /dev/null +++ b/deploy/config_management/BUILD @@ -0,0 +1,45 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Deployment of the S-CORE config management module (layer 2, see //deploy:README.md). + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") + +package(default_visibility = ["//deploy:__pkg__"]) + +# Config daemon binary (0777), placed as /usr/bin/config_management/config_daemon. +pkg_files( + name = "config_daemon_bin", + srcs = ["@score_config_management//score/config_management/config_daemon/code:config_daemon"], + attributes = pkg_attributes(mode = "0777"), + prefix = "usr/bin/config_management", + strip_prefix = strip_prefix.files_only(), +) + +# Config daemon configuration (0644), placed under +# /usr/bin/config_management/etc. Both files are shipped by the upstream +# module, so they belong to the deployment and not to the showcase that +# drives the daemon. +pkg_files( + name = "config_daemon_configs", + srcs = [ + "@score_config_management//score/config_management/config_daemon/code/plugins/demo_json:demo_parameters", + "@score_config_management//score/config_management/config_daemon/code/services/details/mw_com:mw_com_config", + ], + attributes = pkg_attributes(mode = "0644"), + prefix = "usr/bin/config_management/etc", + renames = { + "@score_config_management//score/config_management/config_daemon/code/services/details/mw_com:mw_com_config": "mw_com_config.json", + "@score_config_management//score/config_management/config_daemon/code/plugins/demo_json:demo_parameters": "demo_parameters.json", + }, +) diff --git a/deploy/lifecycle/BUILD b/deploy/lifecycle/BUILD new file mode 100644 index 00000000000..0c1ea3239b7 --- /dev/null +++ b/deploy/lifecycle/BUILD @@ -0,0 +1,32 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Deployment of the S-CORE lifecycle module (layer 2, see //deploy:README.md). +# +# Only the launch manager daemon is deployed here. The applications it +# supervises, and the launch manager configuration that declares them, are +# payload of whoever uses the platform - for this repository that is the +# lifecycle showcase in //showcases/simple_lifecycle. + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") + +package(default_visibility = ["//deploy:__pkg__"]) + +# Launch manager binary (0777), placed as /usr/bin/launch_manager. +pkg_files( + name = "launch_manager_bin", + srcs = ["@score_lifecycle//score/launch_manager"], + attributes = pkg_attributes(mode = "0777"), + prefix = "usr/bin", + strip_prefix = strip_prefix.files_only(), +) diff --git a/deploy/logging/BUILD b/deploy/logging/BUILD new file mode 100644 index 00000000000..24318d7427b --- /dev/null +++ b/deploy/logging/BUILD @@ -0,0 +1,39 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Deployment of the S-CORE logging module (layer 2, see //deploy:README.md). + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") + +package(default_visibility = ["//deploy:__pkg__"]) + +# Datarouter binary (0777), placed under /usr/bin/datarouter. +pkg_files( + name = "datarouter_bin", + srcs = ["@score_logging//score/datarouter"], + attributes = pkg_attributes(mode = "0777"), + prefix = "usr/bin/datarouter", + strip_prefix = strip_prefix.files_only(), +) + +# Datarouter configuration files (0644), placed under /usr/bin/datarouter/etc. +pkg_files( + name = "datarouter_configs", + srcs = [ + "//feature_integration_tests/configs/datarouter:etc/log-channels.json", + "//feature_integration_tests/configs/datarouter:etc/logging.json", + ], + attributes = pkg_attributes(mode = "0644"), + prefix = "usr/bin/datarouter/etc", + strip_prefix = strip_prefix.files_only(), +) diff --git a/docs/integration_process/step_4_platforms.rst b/docs/integration_process/step_4_platforms.rst index 90445ac3cc5..dacdb495118 100644 --- a/docs/integration_process/step_4_platforms.rst +++ b/docs/integration_process/step_4_platforms.rst @@ -176,39 +176,64 @@ job** automatically. .. _add_directly: -Add a binary directly to an image ---------------------------------- - -If your component is not a CLI showcase — for example a background daemon that -must always be present — you wire it straight into each image's build -description instead. Unlike the showcase route, **this is per-image work: you -repeat it for every image you target** (Linux, QNX, AutoSD, EBcLfSA). - -The reference example is the ``datarouter``. Its destination path, permissions -and owner are declared once as a ``pkg_files`` target; because the binary is -resolved per platform (not per architecture) the definition is shared from -``images/common/apps`` and each image lists it in its ``srcs``. -``score_rules_imagefs`` then generates the matching ``mkifs`` directives, so -nothing has to be added to ``system.build``: +Deploy a component as part of the platform +------------------------------------------ + +If your component is not a showcase — for example a background daemon that must +always be present — it belongs to the *deployed platform* rather than to the +demonstration layer. The image content is split into three layers: + +* **Image** (``images/``) — boot, kernel, drivers, rootfs skeleton. +* **Platform** (``deploy/``) — the S-CORE modules as deployed. This is what a + conformance check treats as the system under test. +* **Showcases** (``showcases/``) — applications that demonstrate and verify the + platform, removable without touching the two layers below. + +Keep the two apart: a showcase must never ship the component it exercises, +otherwise it verifies the binary it brought along instead of the one the +distribution deployed. The lifecycle module is the illustrative case — the +``launch_manager`` daemon is deployed from ``deploy/lifecycle``, while the +applications it supervises stay in ``showcases/simple_lifecycle``. + +**1. Package the component.** Declare destination path and permissions as a +``pkg_files`` target in ``deploy//BUILD``. Because the binary is +resolved per platform (not per architecture), one definition serves every +image, and ``score_rules_imagefs`` generates the matching ``mkifs`` directives +so nothing has to be added to ``system.build``: .. code-block:: python - # images/common/apps/BUILD (defined once, shared by every image) + # deploy/logging/BUILD pkg_files( name = "datarouter_bin", srcs = ["@score_logging//score/datarouter"], attributes = pkg_attributes(mode = "0777"), prefix = "usr/bin/datarouter", strip_prefix = strip_prefix.files_only(), + ) + +**2. Register it in the platform bundle.** Unlike the showcase route this is +*not* per-image work: every image already consumes the aggregate, so adding +your target to it is enough. + +.. code-block:: python + + # deploy/BUILD + pkg_filegroup( + name = "platform_bundle", + srcs = [ + # ... + "//deploy/logging:datarouter_bin", + ], visibility = ["//visibility:public"], ) - # images/qnx_x86_64/build/BUILD (referenced per image) + # images/qnx_x86_64/build/BUILD (already present, shown for context) qnx_ifs( name = "init", srcs = [ # ... - "//images/common/apps:datarouter_bin", + "//deploy:platform_bundle", ], build_file = "init.build", extra_build_files = ["system.build"], @@ -218,12 +243,10 @@ Note that ``qnx_ifs`` rejects anything in ``srcs`` that does not provide a ``rules_pkg`` provider, so a plain ``filegroup`` or a bare label will not work - wrap it in ``pkg_files``. -See `images/common/apps/BUILD `_ -and `images/qnx_x86_64/build/BUILD `_ -for the full picture, and repeat the equivalent wiring in the other images you -need (`images/linux_x86_64 `_, -`images/autosd `_, -`images/ebclfsa_aarch64 `_). +See `deploy/README.md `_ +for the contract between the platform and the showcase layer, and +`images/qnx_x86_64/build/BUILD `_ +for how an image consumes it. If your module simply cannot run on a given platform, exclude its targets via ``metadata.exclude_test_targets`` / ``metadata.extra_test_config`` in diff --git a/images/common/apps/BUILD b/images/common/apps/BUILD deleted file mode 100644 index d5daaa5f623..00000000000 --- a/images/common/apps/BUILD +++ /dev/null @@ -1,68 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -# Architecture-independent application packaging shared by the per-image targets. -# -# These pkg_files reference external module binaries/configs that Bazel resolves -# per platform (not per architecture), so a single definition works for every -# image. Consumers: -# - QNX images (qnx_x86_64, qnx_aarch64): reference these directly in qnx_ifs srcs. -# - Linux image (linux_x86_64): wraps them in a pkg_tar for oci_image. -# autosd/ebclfsa images do not ship these applications. - -load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") - -# Datarouter binary (0777), placed under /usr/bin/datarouter. -pkg_files( - name = "datarouter_bin", - srcs = ["@score_logging//score/datarouter"], - attributes = pkg_attributes(mode = "0777"), - prefix = "usr/bin/datarouter", - strip_prefix = strip_prefix.files_only(), - visibility = ["//visibility:public"], -) - -# Datarouter configuration files (0644), placed under /usr/bin/datarouter/etc. -pkg_files( - name = "datarouter_configs", - srcs = [ - "//feature_integration_tests/configs/datarouter:etc/log-channels.json", - "//feature_integration_tests/configs/datarouter:etc/logging.json", - ], - attributes = pkg_attributes(mode = "0644"), - prefix = "usr/bin/datarouter/etc", - strip_prefix = strip_prefix.files_only(), - visibility = ["//visibility:public"], -) - -# Communication framework configuration in /etc (0777). -pkg_files( - name = "etc_comm_configs", - srcs = [ - "//feature_integration_tests/configs:etc/logging.json", - "//feature_integration_tests/configs:etc/mw_com_config.json", - ], - attributes = pkg_attributes(mode = "0777"), - prefix = "etc", - strip_prefix = strip_prefix.files_only(), - visibility = ["//visibility:public"], -) - -# Persistency C++ test scenarios binary, placed as /cpp_tests_persistency (0777). -pkg_files( - name = "cpp_tests_persistency_bin", - srcs = ["@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios"], - attributes = pkg_attributes(mode = "0777"), - renames = {"@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios": "cpp_tests_persistency"}, - visibility = ["//visibility:public"], -) diff --git a/images/linux_x86_64/BUILD b/images/linux_x86_64/BUILD index 034a9e7683d..6e644736013 100644 --- a/images/linux_x86_64/BUILD +++ b/images/linux_x86_64/BUILD @@ -11,7 +11,6 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load") -load("@rules_pkg//pkg:tar.bzl", "pkg_tar") load("@rules_shell//shell:sh_binary.bzl", "sh_binary") sh_binary( @@ -30,21 +29,12 @@ sh_binary( }, ) -# Datarouter binary and configs, shared via //images/common/apps. -pkg_tar( - name = "datarouter_tar", - srcs = [ - "//images/common/apps:datarouter_bin", - "//images/common/apps:datarouter_configs", - ], -) - oci_image( name = "image", base = "@ubuntu_22_04_linux_amd64", tars = [ + "//deploy:platform_tar", "//showcases:showcases_pkg_tar", - ":datarouter_tar", ], ) diff --git a/images/qnx_aarch64/build/BUILD b/images/qnx_aarch64/build/BUILD index a219df4f872..1ba13c80c90 100644 --- a/images/qnx_aarch64/build/BUILD +++ b/images/qnx_aarch64/build/BUILD @@ -14,18 +14,14 @@ load("@score_rules_imagefs//rules/qnx:ifs.bzl", "qnx_ifs") # The architecture-independent configuration files are shared via -# //images/qnx_common/configs; the application packaging (datarouter -# binary/configs, communication configs, persistency test binary) via -# //images/common/apps. The aarch64 QEMU virt image has no arch-specific -# configuration files of its own. +# //images/qnx_common/configs; the deployed S-CORE platform comes from +# //deploy and the applications demonstrating it from //showcases. The +# aarch64 QEMU virt image has no arch-specific configuration files of its own. qnx_ifs( name = "init", srcs = [ - "//images/common/apps:cpp_tests_persistency_bin", - "//images/common/apps:datarouter_bin", - "//images/common/apps:datarouter_configs", - "//images/common/apps:etc_comm_configs", + "//deploy:platform_bundle", "//images/qnx_common/configs:etc_configs", "//images/qnx_common/configs:etc_network_capture", "//images/qnx_common/configs:etc_scripts", diff --git a/images/qnx_x86_64/build/BUILD b/images/qnx_x86_64/build/BUILD index 1306c8ffa0b..624b8b10638 100644 --- a/images/qnx_x86_64/build/BUILD +++ b/images/qnx_x86_64/build/BUILD @@ -43,19 +43,15 @@ pkg_files( ) # The architecture-independent configuration files are shared via -# //images/qnx_common/configs; the application packaging (datarouter -# binary/configs, communication configs, persistency test binary) via -# //images/common/apps. +# //images/qnx_common/configs; the deployed S-CORE platform comes from +# //deploy and the applications demonstrating it from //showcases. qnx_ifs( name = "init", srcs = [ ":etc_startup_hw", ":pci_configs", - "//images/common/apps:cpp_tests_persistency_bin", - "//images/common/apps:datarouter_bin", - "//images/common/apps:datarouter_configs", - "//images/common/apps:etc_comm_configs", + "//deploy:platform_bundle", "//images/qnx_common/configs:etc_configs", "//images/qnx_common/configs:etc_network_capture", "//images/qnx_common/configs:etc_scripts", diff --git a/showcases/BUILD b/showcases/BUILD index 7f5f081dedd..2d9554a0950 100644 --- a/showcases/BUILD +++ b/showcases/BUILD @@ -22,6 +22,7 @@ score_pkg_bundle( "//showcases/standalone:time_pkg_files", "//showcases/standalone:logging_pkg_files", "//showcases/standalone:config_management_pkg_files", + "//showcases/standalone:persistency_pkg_files", "//showcases/simple_lifecycle:simple_lifecycle_pkg_files", ], package_dir = "showcases", @@ -40,6 +41,7 @@ pkg_filegroup( "//showcases/standalone:config_management_pkg_files", "//showcases/standalone:kyron_pkg_files", "//showcases/standalone:logging_pkg_files", + "//showcases/standalone:persistency_pkg_files", "//showcases/standalone:time_pkg_files", ], prefix = "showcases", diff --git a/showcases/simple_lifecycle/BUILD b/showcases/simple_lifecycle/BUILD index 37afd26f68f..ba609151fe1 100644 --- a/showcases/simple_lifecycle/BUILD +++ b/showcases/simple_lifecycle/BUILD @@ -20,10 +20,12 @@ launch_manager_config( flatbuffer_out_dir = "etc", ) +# The launch manager itself is part of the deployed platform +# (//deploy/lifecycle), not of this showcase. What ships here are the +# applications it supervises and the configuration that declares them. score_pkg_bundle( name = "simple_lifecycle", bins = [ - "@score_lifecycle//score/launch_manager:launch_manager", "@score_lifecycle//examples/cpp_supervised_app", "@score_lifecycle//examples/rust_supervised_app", "@score_lifecycle//examples/control_application:control_daemon", diff --git a/showcases/simple_lifecycle/simple_lifecycle.score.json b/showcases/simple_lifecycle/simple_lifecycle.score.json index 5b05a6b83bf..39ef65d4871 100644 --- a/showcases/simple_lifecycle/simple_lifecycle.score.json +++ b/showcases/simple_lifecycle/simple_lifecycle.score.json @@ -3,7 +3,7 @@ "description": "Example runs two applications and monitors if they are healthy. When some application is malfunctioning, this will be recorded. Examples takes ~15s", "apps": [ { - "path": "/showcases/bin/launch_manager", + "path": "/usr/bin/launch_manager", "dir": "/showcases/data/simple_lifecycle", "args": [], "env": {} diff --git a/showcases/standalone/BUILD b/showcases/standalone/BUILD index 0bb5b946e8d..3b4b8c1f391 100644 --- a/showcases/standalone/BUILD +++ b/showcases/standalone/BUILD @@ -10,6 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") load("//bazel_common:bundlers.bzl", "score_pkg_bundle") score_pkg_bundle( @@ -67,17 +68,24 @@ score_pkg_bundle( package_dir = "standalone", ) +# The config daemon and its configuration are part of the deployed platform +# (//deploy/config_management). This showcase only contributes the manifest +# that drives the deployed daemon. score_pkg_bundle( name = "config_management", - bins = [ - "@score_config_management//score/config_management/config_daemon/code:config_daemon", - ], + bins = [], config_data = [ "//showcases/standalone:config_management.score.json", ], - custom_layout = { - "@score_config_management//score/config_management/config_daemon/code/services/details/mw_com:mw_com_config": "etc/mw_com_config.json", - "@score_config_management//score/config_management/config_daemon/code/plugins/demo_json:demo_parameters": "etc/demo_parameters.json", - }, package_dir = "standalone", ) + +# Persistency has no runnable showcase yet; the upstream test scenarios binary +# is shipped so that one can be added without changing the image layout. +pkg_files( + name = "persistency_pkg_files", + srcs = ["@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios"], + attributes = pkg_attributes(mode = "0755"), + renames = {"@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios": "bin/cpp_tests_persistency"}, + visibility = ["//visibility:public"], +) diff --git a/showcases/standalone/config_management.score.json b/showcases/standalone/config_management.score.json index bebc27d87be..a82d582f41f 100644 --- a/showcases/standalone/config_management.score.json +++ b/showcases/standalone/config_management.score.json @@ -3,12 +3,12 @@ "description": "Example for running the config management daemon with demo parameters (non functional yet)", "apps": [ { - "path": "/showcases/bin/config_daemon", + "path": "/usr/bin/config_management/config_daemon", "args": [ - "-s", "/showcases/data/config_management/etc/mw_com_config.json" + "-s", "/usr/bin/config_management/etc/mw_com_config.json" ], "env": {}, - "dir": "/showcases/data/config_management" + "dir": "/usr/bin/config_management" } ] } From d1693343b748ba33de66ecec8b1b22dc135f31a2 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 14 Oct 2026 13:26:05 +0000 Subject: [PATCH 2/8] Move deployed configuration out of the test directory The datarouter and mw::com configuration files lived under feature_integration_tests/configs/ but were never read by the feature integration tests. Their only consumers are the deployment packages and the ebclfsa image, so they belong next to the module they configure. Files move unchanged; the packaged paths stay /usr/bin/datarouter/etc/* and /etc/*. What remains in feature_integration_tests/configs is the test harness configuration (dlt_config_*, qemu_bridge_config). --- deploy/README.md | 5 ++-- deploy/communication/BUILD | 15 +++++++++-- .../communication}/etc/logging.json | 0 .../communication}/etc/mw_com_config.json | 0 deploy/logging/BUILD | 4 +-- .../logging}/etc/log-channels.json | 0 .../logging}/etc/logging.json | 0 feature_integration_tests/configs/BUILD | 16 ++---------- .../configs/datarouter/BUILD | 26 ------------------- images/ebclfsa_aarch64/build/BUILD | 2 +- 10 files changed, 21 insertions(+), 47 deletions(-) rename {feature_integration_tests/configs => deploy/communication}/etc/logging.json (100%) rename {feature_integration_tests/configs => deploy/communication}/etc/mw_com_config.json (100%) rename {feature_integration_tests/configs/datarouter => deploy/logging}/etc/log-channels.json (100%) rename {feature_integration_tests/configs/datarouter => deploy/logging}/etc/logging.json (100%) delete mode 100644 feature_integration_tests/configs/datarouter/BUILD diff --git a/deploy/README.md b/deploy/README.md index e7710771e1b..64c0d0e9bc2 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -49,8 +49,9 @@ daemon. Add a `deploy//BUILD` that packages the module's binaries and configuration with `pkg_files`, then list those targets in -`//deploy:platform_bundle`. Keep examples, demo applications and test -binaries out; those belong in `//showcases`. +`//deploy:platform_bundle`. Configuration that is deployed onto the target +is kept in `deploy//etc/`, not in a test directory. Keep examples, +demo applications and test binaries out; those belong in `//showcases`. Long term each S-CORE module should export its own deployment package upstream, so that "deployed correctly" becomes checkable rather than agreed. diff --git a/deploy/communication/BUILD b/deploy/communication/BUILD index 8d15117d677..f6838244913 100644 --- a/deploy/communication/BUILD +++ b/deploy/communication/BUILD @@ -23,10 +23,21 @@ package(default_visibility = ["//deploy:__pkg__"]) pkg_files( name = "etc_comm_configs", srcs = [ - "//feature_integration_tests/configs:etc/logging.json", - "//feature_integration_tests/configs:etc/mw_com_config.json", + "etc/logging.json", + "etc/mw_com_config.json", ], attributes = pkg_attributes(mode = "0777"), prefix = "etc", strip_prefix = strip_prefix.files_only(), ) + +# Same files as a plain filegroup, for images that assemble /etc themselves +# instead of consuming rules_pkg providers. +filegroup( + name = "etc_configs", + srcs = [ + "etc/logging.json", + "etc/mw_com_config.json", + ], + visibility = ["//visibility:public"], +) diff --git a/feature_integration_tests/configs/etc/logging.json b/deploy/communication/etc/logging.json similarity index 100% rename from feature_integration_tests/configs/etc/logging.json rename to deploy/communication/etc/logging.json diff --git a/feature_integration_tests/configs/etc/mw_com_config.json b/deploy/communication/etc/mw_com_config.json similarity index 100% rename from feature_integration_tests/configs/etc/mw_com_config.json rename to deploy/communication/etc/mw_com_config.json diff --git a/deploy/logging/BUILD b/deploy/logging/BUILD index 24318d7427b..6c6930cb356 100644 --- a/deploy/logging/BUILD +++ b/deploy/logging/BUILD @@ -30,8 +30,8 @@ pkg_files( pkg_files( name = "datarouter_configs", srcs = [ - "//feature_integration_tests/configs/datarouter:etc/log-channels.json", - "//feature_integration_tests/configs/datarouter:etc/logging.json", + "etc/log-channels.json", + "etc/logging.json", ], attributes = pkg_attributes(mode = "0644"), prefix = "usr/bin/datarouter/etc", diff --git a/feature_integration_tests/configs/datarouter/etc/log-channels.json b/deploy/logging/etc/log-channels.json similarity index 100% rename from feature_integration_tests/configs/datarouter/etc/log-channels.json rename to deploy/logging/etc/log-channels.json diff --git a/feature_integration_tests/configs/datarouter/etc/logging.json b/deploy/logging/etc/logging.json similarity index 100% rename from feature_integration_tests/configs/datarouter/etc/logging.json rename to deploy/logging/etc/logging.json diff --git a/feature_integration_tests/configs/BUILD b/feature_integration_tests/configs/BUILD index dce9a78284e..1b1e01d73b2 100644 --- a/feature_integration_tests/configs/BUILD +++ b/feature_integration_tests/configs/BUILD @@ -10,6 +10,8 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +# Test harness configuration. Configuration that is deployed onto the target +# belongs to //deploy, not here. exports_files( [ "dlt_config_qnx_x86_64.json", @@ -17,17 +19,3 @@ exports_files( "qemu_bridge_config.json", ], ) - -exports_files([ - "etc/logging.json", - "etc/mw_com_config.json", -]) - -filegroup( - name = "etc_configs", - srcs = [ - "etc/logging.json", - "etc/mw_com_config.json", - ], - visibility = ["//visibility:public"], -) diff --git a/feature_integration_tests/configs/datarouter/BUILD b/feature_integration_tests/configs/datarouter/BUILD deleted file mode 100644 index ab722788def..00000000000 --- a/feature_integration_tests/configs/datarouter/BUILD +++ /dev/null @@ -1,26 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -exports_files([ - "etc/logging.json", - "etc/log-channels.json", -]) - -filegroup( - name = "etc_configs", - srcs = [ - "etc/log-channels.json", - "etc/logging.json", - ], - visibility = ["//visibility:public"], -) diff --git a/images/ebclfsa_aarch64/build/BUILD b/images/ebclfsa_aarch64/build/BUILD index b5d66fbafe6..3dc353498c7 100644 --- a/images/ebclfsa_aarch64/build/BUILD +++ b/images/ebclfsa_aarch64/build/BUILD @@ -47,7 +47,7 @@ genrule( name = "fastdev-image", srcs = [ "//showcases:showcases_pkg_tar", - "//feature_integration_tests/configs:etc_configs", + "//deploy/communication:etc_configs", ":extract-fastdev-image", ":crinit_config_tar", ":scripts_tar", From 58d8efc505486b805d5837cab7c690f51d2b1204 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Thu, 15 Oct 2026 09:34:57 +0000 Subject: [PATCH 3/8] Bundle the persistency showcase like the other standalone showcases Use score_pkg_bundle instead of a hand-written pkg_files. An alias keeps the installed binary name bin/cpp_tests_persistency, since score_pkg_bundle names binaries after their label. --- showcases/standalone/BUILD | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/showcases/standalone/BUILD b/showcases/standalone/BUILD index 3b4b8c1f391..445c11f25ee 100644 --- a/showcases/standalone/BUILD +++ b/showcases/standalone/BUILD @@ -10,7 +10,6 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") load("//bazel_common:bundlers.bzl", "score_pkg_bundle") score_pkg_bundle( @@ -82,10 +81,14 @@ score_pkg_bundle( # Persistency has no runnable showcase yet; the upstream test scenarios binary # is shipped so that one can be added without changing the image layout. -pkg_files( - name = "persistency_pkg_files", - srcs = ["@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios"], - attributes = pkg_attributes(mode = "0755"), - renames = {"@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios": "bin/cpp_tests_persistency"}, - visibility = ["//visibility:public"], +# score_pkg_bundle names binaries after their label, hence the alias. +alias( + name = "cpp_tests_persistency", + actual = "@score_persistency//score/kvs/tests/test_scenarios/cpp:test_scenarios", +) + +score_pkg_bundle( + name = "persistency", + bins = ["//showcases/standalone:cpp_tests_persistency"], + package_dir = "standalone", ) From 6c7ff63f8b11c5c615db7e209d1f4cbcb39d6f91 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Thu, 15 Oct 2026 12:53:28 +0000 Subject: [PATCH 4/8] ci: run platform integration tests on QNX x86_64 The showcases are shipped in the QNX image, but only the ITF suite ran there, so none of them was ever executed on QNX. Run //platform_integration_tests:pit against the same QEMU image. --- .github/workflows/build_and_test_qnx.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_and_test_qnx.yml b/.github/workflows/build_and_test_qnx.yml index 54e8dbd5039..dd5cf9aadef 100644 --- a/.github/workflows/build_and_test_qnx.yml +++ b/.github/workflows/build_and_test_qnx.yml @@ -76,6 +76,9 @@ jobs: - name: Run integration tests run: | bazel test --lockfile_mode=error --config=itf-qnx-x86_64 //feature_integration_tests/itf + - name: Run platform integration tests + run: | + bazel test --lockfile_mode=error --config=itf-qnx-x86_64 //platform_integration_tests:pit - name: Cleanup QNX license if: always() run: sudo rm -rf /opt/score_qnx From ebffe7ef736c2ae4830cf3cb953105a22a1b8a69 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Thu, 29 Oct 2026 09:26:11 +0000 Subject: [PATCH 5/8] Remove the unused communication deployment package deploy/communication shipped /etc/mw_com_config.json and /etc/logging.json, a leftover of the scrample demo (service /score/MapApiLanesStamped, appId IPBR). Nothing reads them: mw::com has no system-wide configuration (it reads ./etc/mw_com_config.json relative to the working directory or the file passed with -s), mw::log's only system-wide file is /etc/ecu_logging_config.json, and every showcase brings its own configuration. Communication ships as a library, so it has nothing to deploy. Drops the package from //deploy:platform_bundle and from the EB corbos image genrule (where it was a dependency only, never passed to build_image.sh), and corrects the layer 2 / layer 3 contract in deploy/README.md. --- deploy/BUILD | 1 - deploy/README.md | 11 +++- deploy/communication/BUILD | 43 -------------- deploy/communication/etc/logging.json | 8 --- deploy/communication/etc/mw_com_config.json | 63 --------------------- images/ebclfsa_aarch64/build/BUILD | 1 - 6 files changed, 10 insertions(+), 117 deletions(-) delete mode 100644 deploy/communication/BUILD delete mode 100644 deploy/communication/etc/logging.json delete mode 100644 deploy/communication/etc/mw_com_config.json diff --git a/deploy/BUILD b/deploy/BUILD index 0a02b93d4af..f1858621098 100644 --- a/deploy/BUILD +++ b/deploy/BUILD @@ -25,7 +25,6 @@ exports_files(["README.md"]) pkg_filegroup( name = "platform_bundle", srcs = [ - "//deploy/communication:etc_comm_configs", "//deploy/config_management:config_daemon_bin", "//deploy/config_management:config_daemon_configs", "//deploy/lifecycle:launch_manager_bin", diff --git a/deploy/README.md b/deploy/README.md index 64c0d0e9bc2..0122f56d32e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -37,7 +37,14 @@ Layer 3 may rely on the following, and on nothing else: | `launch_manager` | `/usr/bin/launch_manager` | | `datarouter` | `/usr/bin/datarouter/datarouter`, config in `/usr/bin/datarouter/etc` | | `config_daemon` | `/usr/bin/config_management/config_daemon`, config in `/usr/bin/config_management/etc` | -| mw::com system configuration | `/etc/mw_com_config.json` | + +There is no system-wide mw::com configuration: mw::com reads +`./etc/mw_com_config.json` relative to the working directory, or the file +passed with `-s`. mw::log only has an optional ECU-wide +`/etc/ecu_logging_config.json`; the application configuration +(`etc/logging.json` next to the executable or in the working directory, or +`MW_LOG_CONFIG_FILE`) belongs to the application. Every application ships its +own configuration. Layer 2 never references anything under `/showcases`. The one place where layer 2 reads layer 3 data is the launch manager, which is started by the @@ -52,6 +59,8 @@ configuration with `pkg_files`, then list those targets in `//deploy:platform_bundle`. Configuration that is deployed onto the target is kept in `deploy//etc/`, not in a test directory. Keep examples, demo applications and test binaries out; those belong in `//showcases`. +Modules that ship only libraries (for example communication or persistency) +have nothing to deploy and get no package here. Long term each S-CORE module should export its own deployment package upstream, so that "deployed correctly" becomes checkable rather than agreed. diff --git a/deploy/communication/BUILD b/deploy/communication/BUILD deleted file mode 100644 index f6838244913..00000000000 --- a/deploy/communication/BUILD +++ /dev/null @@ -1,43 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -# Deployment of the S-CORE communication module (layer 2, see //deploy:README.md). -# -# Communication ships as a library, so the only deployed artifacts are the -# system-wide configuration files every mw::com participant reads from /etc. - -load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") - -package(default_visibility = ["//deploy:__pkg__"]) - -pkg_files( - name = "etc_comm_configs", - srcs = [ - "etc/logging.json", - "etc/mw_com_config.json", - ], - attributes = pkg_attributes(mode = "0777"), - prefix = "etc", - strip_prefix = strip_prefix.files_only(), -) - -# Same files as a plain filegroup, for images that assemble /etc themselves -# instead of consuming rules_pkg providers. -filegroup( - name = "etc_configs", - srcs = [ - "etc/logging.json", - "etc/mw_com_config.json", - ], - visibility = ["//visibility:public"], -) diff --git a/deploy/communication/etc/logging.json b/deploy/communication/etc/logging.json deleted file mode 100644 index 8b3d1eacc88..00000000000 --- a/deploy/communication/etc/logging.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "appId": "IPBR", - "appDesc": "ipc_bridge", - "logLevel": "kDebug", - "logLevelThresholdConsole": "kInfo", - "logMode": "kConsole", - "dynamicDatarouterIdentifiers" : true -} diff --git a/deploy/communication/etc/mw_com_config.json b/deploy/communication/etc/mw_com_config.json deleted file mode 100644 index 1e104bd4843..00000000000 --- a/deploy/communication/etc/mw_com_config.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "serviceTypes": [ - { - "serviceTypeName": "/score/MapApiLanesStamped", - "version": { - "major": 1, - "minor": 0 - }, - "bindings": [ - { - "binding": "SHM", - "serviceId": 6432, - "events": [ - { - "eventName": "map_api_lanes_stamped", - "eventId": 1 - }, - { - "eventName": "dummy_data_stamped", - "eventId": 2 - } - ] - } - ] - } - ], - "serviceInstances": [ - { - "instanceSpecifier": "score/MapApiLanesStamped", - "serviceTypeName": "/score/MapApiLanesStamped", - "version": { - "major": 1, - "minor": 0 - }, - "instances": [ - { - "instanceId": 1, - "allowedConsumer": { - "QM": [ - 4002, - 0 - ] - }, - "allowedProvider": { - "QM": [ - 4001, - 0 - ] - }, - "asil-level": "QM", - "binding": "SHM", - "events": [ - { - "eventName": "map_api_lanes_stamped", - "numberOfSampleSlots": 10, - "maxSubscribers": 3 - } - ] - } - ] - } - ] -} diff --git a/images/ebclfsa_aarch64/build/BUILD b/images/ebclfsa_aarch64/build/BUILD index 3dc353498c7..ada32854d62 100644 --- a/images/ebclfsa_aarch64/build/BUILD +++ b/images/ebclfsa_aarch64/build/BUILD @@ -47,7 +47,6 @@ genrule( name = "fastdev-image", srcs = [ "//showcases:showcases_pkg_tar", - "//deploy/communication:etc_configs", ":extract-fastdev-image", ":crinit_config_tar", ":scripts_tar", From e8044f007180b80c92f5163f5c1d5baca875c1b0 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 30 Oct 2026 10:05:51 +0000 Subject: [PATCH 6/8] Deploy a heartbeat service that reports the S-CORE version Communication and baselibs ship as libraries, so the platform had nothing a layer 3 check could exercise for them. The platform now deploys a small mw::com service built on both (//deploy/communication), and the communication showcase gains a consumer that brings no provider of its own. The service score/platform/heartbeat offers - the heartbeat event: a sequence number and payload sealed with a baselibs CRC32, published every 100 ms, and - the platform_info field: the known_good.json timestamp and the version (or pinned commit) of every target_sw module, generated at build time from //:known_good.json. heartbeat_consumer exits 0 only if it finds the deployed service via service discovery, receives platform_info with an intact CRC32, the platform reports the S-CORE version the consumer was built for (differences are listed per module), and it receives 10 intact heartbeats. The showcase CLI ignores app exit codes, so the platform integration test checks the consumer's output. The reported version is self-declared: it catches a wrongly assembled platform but is no proof of origin. deploy/communication/README.md describes the service, the consumer and these limitations. --- deploy/BUILD | 2 + deploy/README.md | 8 +- deploy/communication/BUILD | 84 +++++++ deploy/communication/README.md | 87 +++++++ deploy/communication/etc/mw_com_config.json | 60 +++++ deploy/communication/gen_platform_info.py | 68 +++++ deploy/communication/heartbeat.cpp | 235 ++++++++++++++++++ deploy/communication/heartbeat.h | 93 +++++++ deploy/communication/heartbeat_service.cpp | 100 ++++++++ .../test_communication_showcase.py | 20 ++ platform_integration_tests/showcase_runner.py | 6 +- showcases/standalone/BUILD | 19 +- showcases/standalone/com_heartbeat.score.json | 24 ++ showcases/standalone/heartbeat_consumer.cpp | 177 +++++++++++++ 14 files changed, 978 insertions(+), 5 deletions(-) create mode 100644 deploy/communication/BUILD create mode 100644 deploy/communication/README.md create mode 100644 deploy/communication/etc/mw_com_config.json create mode 100644 deploy/communication/gen_platform_info.py create mode 100644 deploy/communication/heartbeat.cpp create mode 100644 deploy/communication/heartbeat.h create mode 100644 deploy/communication/heartbeat_service.cpp create mode 100644 showcases/standalone/com_heartbeat.score.json create mode 100644 showcases/standalone/heartbeat_consumer.cpp diff --git a/deploy/BUILD b/deploy/BUILD index f1858621098..b8e1d72f4b7 100644 --- a/deploy/BUILD +++ b/deploy/BUILD @@ -25,6 +25,8 @@ exports_files(["README.md"]) pkg_filegroup( name = "platform_bundle", srcs = [ + "//deploy/communication:heartbeat_service_bin", + "//deploy/communication:heartbeat_service_configs", "//deploy/config_management:config_daemon_bin", "//deploy/config_management:config_daemon_configs", "//deploy/lifecycle:launch_manager_bin", diff --git a/deploy/README.md b/deploy/README.md index 0122f56d32e..9907b881918 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -37,6 +37,7 @@ Layer 3 may rely on the following, and on nothing else: | `launch_manager` | `/usr/bin/launch_manager` | | `datarouter` | `/usr/bin/datarouter/datarouter`, config in `/usr/bin/datarouter/etc` | | `config_daemon` | `/usr/bin/config_management/config_daemon`, config in `/usr/bin/config_management/etc` | +| `heartbeat_service` | `/usr/bin/communication/heartbeat_service`, config in `/usr/bin/communication/etc`; offers the mw::com service `score/platform/heartbeat` with the `heartbeat` event and the `platform_info` field, which reports the S-CORE module versions from `known_good.json` (interface in `deploy/communication/heartbeat.h`) | There is no system-wide mw::com configuration: mw::com reads `./etc/mw_com_config.json` relative to the working directory, or the file @@ -59,8 +60,11 @@ configuration with `pkg_files`, then list those targets in `//deploy:platform_bundle`. Configuration that is deployed onto the target is kept in `deploy//etc/`, not in a test directory. Keep examples, demo applications and test binaries out; those belong in `//showcases`. -Modules that ship only libraries (for example communication or persistency) -have nothing to deploy and get no package here. +Modules that ship only libraries (for example persistency) have nothing to +deploy and get no package here. Communication is the exception: a library +cannot be checked from outside the process that links it, so the platform +deploys a heartbeat service built on it. The communication showcase consumes +that service and fails unless the platform provides it. Long term each S-CORE module should export its own deployment package upstream, so that "deployed correctly" becomes checkable rather than agreed. diff --git a/deploy/communication/BUILD b/deploy/communication/BUILD new file mode 100644 index 00000000000..9d8f7c0d303 --- /dev/null +++ b/deploy/communication/BUILD @@ -0,0 +1,84 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Deployment of the S-CORE communication module (layer 2, see //deploy:README.md). +# +# Communication itself ships as a library. To make "the platform provides +# working inter-process communication" checkable, the platform deploys a +# heartbeat service on top of it; consumers in layer 3 must find it via +# mw::com service discovery and receive intact heartbeats from it. + +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", "strip_prefix") +load("@rules_python//python:defs.bzl", "py_binary") + +package(default_visibility = ["//deploy:__pkg__"]) + +py_binary( + name = "gen_platform_info", + srcs = ["gen_platform_info.py"], +) + +# The S-CORE module versions the platform is built from, as compile-time constants. +genrule( + name = "platform_info_data", + srcs = ["//:known_good.json"], + outs = ["platform_info_data.h"], + cmd = "$(execpath :gen_platform_info) $(execpath //:known_good.json) $@", + tools = [":gen_platform_info"], +) + +# Service interface and payload check: the layer 2/3 contract of the heartbeat service. +cc_library( + name = "heartbeat_interface", + srcs = [ + "heartbeat.cpp", + ":platform_info_data", + ], + hdrs = ["heartbeat.h"], + visibility = ["//showcases:__subpackages__"], + deps = [ + "@score_baselibs//score/hash/code/crc:crc_ieee", + "@score_communication//score/mw/com", + ], +) + +cc_binary( + name = "heartbeat_service", + srcs = ["heartbeat_service.cpp"], + deps = [ + ":heartbeat_interface", + "@score_baselibs//score/string_manipulation/arguments", + "@score_communication//score/mw/com", + ], +) + +# Heartbeat service binary (0777), placed as /usr/bin/communication/heartbeat_service. +pkg_files( + name = "heartbeat_service_bin", + srcs = [":heartbeat_service"], + attributes = pkg_attributes(mode = "0777"), + prefix = "usr/bin/communication", + strip_prefix = strip_prefix.files_only(), +) + +# mw::com configuration of the heartbeat service (0644), placed under +# /usr/bin/communication/etc. Consumers use this deployed file, so a platform +# that deploys a different service definition fails the check. +pkg_files( + name = "heartbeat_service_configs", + srcs = ["etc/mw_com_config.json"], + attributes = pkg_attributes(mode = "0644"), + prefix = "usr/bin/communication/etc", + strip_prefix = strip_prefix.files_only(), +) diff --git a/deploy/communication/README.md b/deploy/communication/README.md new file mode 100644 index 00000000000..7c87855b7b9 --- /dev/null +++ b/deploy/communication/README.md @@ -0,0 +1,87 @@ +# Platform heartbeat service (communication) + +Communication and baselibs ship as libraries. A library cannot be checked from +outside the process that links it, so the platform deploys a small mw::com +service built on both. A consumer in layer 3 has to find that service and +talk to it; if the platform does not deploy it, or deploys something that does +not speak S-CORE communication, the consumer fails. + +See [`deploy/README.md`](../README.md) for the layering and the layer 2 / +layer 3 contract this service is part of. + +## What is deployed + +| Path on target | Content | +| --- | --- | +| `/usr/bin/communication/heartbeat_service` | Provider of the service | +| `/usr/bin/communication/etc/mw_com_config.json` | mw::com configuration of the service; consumers use this deployed file | + +## Service `score/platform/heartbeat` + +The interface is defined in [`heartbeat.h`](heartbeat.h) and shared by +provider and consumer. + +| Element | Kind | Content | +| --- | --- | --- | +| `heartbeat` | event | One `Heartbeat` every 100 ms: sequence number, a payload derived from it and a baselibs CRC32 over both | +| `platform_info` | field | `PlatformInfo`: the S-CORE version the platform was built from, sealed with a baselibs CRC32 | + +### S-CORE version (`platform_info`) + +`PlatformInfo` holds the `timestamp` of `known_good.json` and, for every +module in its `target_sw` group, the module name and its `version` (or the +pinned commit `hash` if it has no version). + +The values are generated at build time: `gen_platform_info.py` turns +`//:known_good.json` into `platform_info_data.h`. Bazel regenerates the header +whenever `known_good.json` changes, so provider and consumer always carry the +versions they were built from. If the data does not fit the fixed-size +`PlatformInfo` (max. 16 modules, see the sizes in `heartbeat.h`), the build +fails. + +The service publishes the field before it offers the service and logs the +version on start-up: + +```text +heartbeat_service: offering score/platform/heartbeat for S-CORE known_good 2026-08-28T09:19:12Z + score_baselibs ee40fece5abacb28ea641b448446a04175038b17 + ... +``` + +## Consumer + +[`//showcases/standalone:heartbeat_consumer`](../../showcases/standalone/heartbeat_consumer.cpp) +brings no provider of its own. It exits 0 only if it + +1. finds `score/platform/heartbeat` via mw::com service discovery, +2. receives `platform_info` with an intact CRC32, +3. finds the same S-CORE version it was built for (it compiles in the same + generated header) — otherwise it lists the differing modules, and +4. receives 10 heartbeats with an intact CRC32. + +It runs as the showcase `Communication Platform Heartbeat` +([`com_heartbeat.score.json`](../../showcases/standalone/com_heartbeat.score.json)), +which the platform integration test +`test_communication_platform_heartbeat` executes. + +## Running it locally + +```sh +bazel build --config=linux-x86_64 //deploy/communication:heartbeat_service //showcases/standalone:heartbeat_consumer +mkdir -p /tmp/mw_com_lola/service_discovery +cd deploy/communication +../../bazel-bin/deploy/communication/heartbeat_service & +../../bazel-bin/showcases/standalone/heartbeat_consumer --service_instance_manifest etc/mw_com_config.json +kill %1 +``` + +## Limitations + +- The reported version is self-declared. It shows which S-CORE version a + platform claims to be and catches a wrongly assembled platform, but it is + no proof of origin; that needs a comparison of the deployed binaries with + the reference build (hash or SBOM). +- It reflects `known_good.json`, not the patches in `//patches` that are + applied on top of the pinned commits. +- The showcase still starts the service itself. Starting it at boot is up to + the image. diff --git a/deploy/communication/etc/mw_com_config.json b/deploy/communication/etc/mw_com_config.json new file mode 100644 index 00000000000..cc6e97af727 --- /dev/null +++ b/deploy/communication/etc/mw_com_config.json @@ -0,0 +1,60 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/platform/Heartbeat", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 6700, + "events": [ + { + "eventName": "heartbeat", + "eventId": 1 + } + ], + "fields": [ + { + "fieldName": "platform_info", + "fieldId": 2 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "score/platform/heartbeat", + "serviceTypeName": "/score/platform/Heartbeat", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "heartbeat", + "numberOfSampleSlots": 20, + "maxSubscribers": 5 + } + ], + "fields": [ + { + "fieldName": "platform_info", + "numberOfSampleSlots": 2, + "maxSubscribers": 5 + } + ] + } + ] + } + ] +} diff --git a/deploy/communication/gen_platform_info.py b/deploy/communication/gen_platform_info.py new file mode 100644 index 00000000000..d3c1204463c --- /dev/null +++ b/deploy/communication/gen_platform_info.py @@ -0,0 +1,68 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Generates the S-CORE platform version header from known_good.json.""" + +import json +import sys + + +def _cpp_string(value: str) -> str: + return json.dumps(value) + + +def main(known_good_path: str, header_path: str) -> None: + with open(known_good_path, encoding="utf-8") as f: + known_good = json.load(f) + + modules = [] + for name, module in sorted(known_good["modules"]["target_sw"].items()): + version = module.get("version") or module.get("hash") + if not version: + sys.exit(f"{known_good_path}: module {name} has neither 'version' nor 'hash'") + modules.append((name, version)) + + entries = "\n".join(f" ModuleEntry{{{_cpp_string(n)}, {_cpp_string(v)}}}," for n, v in modules) + header = f"""// Generated from known_good.json by //deploy/communication:gen_platform_info. Do not edit. +#ifndef DEPLOY_COMMUNICATION_PLATFORM_INFO_DATA_H +#define DEPLOY_COMMUNICATION_PLATFORM_INFO_DATA_H + +#include +#include + +namespace score::platform::heartbeat::generated +{{ + +struct ModuleEntry +{{ + std::string_view name; + std::string_view version; +}}; + +inline constexpr std::string_view kKnownGoodTimestamp{{{_cpp_string(known_good.get("timestamp", ""))}}}; + +inline constexpr std::array kModules{{{{ +{entries} +}}}}; + +}} // namespace score::platform::heartbeat::generated + +#endif // DEPLOY_COMMUNICATION_PLATFORM_INFO_DATA_H +""" + with open(header_path, "w", encoding="utf-8") as f: + f.write(header) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + sys.exit("usage: gen_platform_info.py ") + main(sys.argv[1], sys.argv[2]) diff --git a/deploy/communication/heartbeat.cpp b/deploy/communication/heartbeat.cpp new file mode 100644 index 00000000000..fa0e5372e7c --- /dev/null +++ b/deploy/communication/heartbeat.cpp @@ -0,0 +1,235 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "deploy/communication/heartbeat.h" + +#include "deploy/communication/platform_info_data.h" +#include "score/hash/code/crc/crc32_ieee.h" + +#include +#include + +namespace score::platform::heartbeat +{ +namespace +{ + +constexpr bool GeneratedDataFits() noexcept +{ + if ((generated::kModules.size() > kMaxModules) || (generated::kKnownGoodTimestamp.size() >= kTimestampSize)) + { + return false; + } + for (const auto& module : generated::kModules) + { + if ((module.name.size() >= kNameSize) || (module.version.size() >= kVersionSize)) + { + return false; + } + } + return true; +} +static_assert(GeneratedDataFits(), "known_good.json does not fit into PlatformInfo; raise the sizes in heartbeat.h"); + +template +void CopyTo(std::array& target, const std::string_view value) noexcept +{ + target.fill('\0'); + std::copy_n(value.begin(), std::min(value.size(), N - 1U), target.begin()); +} + +template +bool IsTerminated(const std::array& value) noexcept +{ + return std::find(value.begin(), value.end(), '\0') != value.end(); +} + +template +std::string_view View(const std::array& value) noexcept +{ + const auto end = std::find(value.begin(), value.end(), '\0'); + return {value.data(), static_cast(end - value.begin())}; +} + +template +score::Result Feed(score::hash::Crc32IeeeHashCalculator& crc, const std::array& value) noexcept +{ + // Whole array incl. zero padding, so the checksum covers every byte of the sample. + return crc.Update({reinterpret_cast(value.data()), value.size()}); +} + +std::optional Crc32Of(const PlatformInfo& info) noexcept +{ + std::array count_bytes{}; + for (std::size_t i = 0U; i < count_bytes.size(); ++i) + { + count_bytes[i] = static_cast((info.module_count >> (8U * i)) & 0xFFU); + } + + score::hash::Crc32IeeeHashCalculator crc{}; + bool ok = Feed(crc, info.known_good_timestamp).has_value() && + crc.Update({count_bytes.data(), count_bytes.size()}).has_value(); + for (const auto& module : info.modules) + { + ok = ok && Feed(crc, module.name).has_value() && Feed(crc, module.version).has_value(); + } + if (!ok) + { + return std::nullopt; + } + return static_cast(crc.GetChecksum()); +} + +const ModuleVersion* FindModule(const PlatformInfo& info, const std::string_view name) noexcept +{ + for (std::size_t i = 0U; i < info.module_count; ++i) + { + if (View(info.modules[i].name) == name) + { + return &info.modules[i]; + } + } + return nullptr; +} + +using Payload = std::array; + +Payload PayloadFor(const std::uint64_t sequence) noexcept +{ + Payload payload{}; + for (std::size_t i = 0U; i < payload.size(); ++i) + { + payload[i] = static_cast((sequence + i) & 0xFFU); + } + return payload; +} + +std::optional Crc32Of(const std::uint64_t sequence, const Payload& payload) noexcept +{ + // Serialized explicitly so the checksum does not depend on struct padding or endianness. + std::array sequence_bytes{}; + for (std::size_t i = 0U; i < sequence_bytes.size(); ++i) + { + sequence_bytes[i] = static_cast((sequence >> (8U * i)) & 0xFFU); + } + + score::hash::Crc32IeeeHashCalculator crc{}; + if (!crc.Update({sequence_bytes.data(), sequence_bytes.size()}).has_value() || + !crc.Update({payload.data(), payload.size()}).has_value()) + { + return std::nullopt; + } + return static_cast(crc.GetChecksum()); +} + +} // namespace + +Heartbeat MakeHeartbeat(const std::uint64_t sequence) noexcept +{ + Heartbeat heartbeat{sequence, PayloadFor(sequence), 0U}; + heartbeat.crc32 = Crc32Of(heartbeat.sequence, heartbeat.payload).value_or(0U); + return heartbeat; +} + +bool IsValid(const Heartbeat& heartbeat) noexcept +{ + if (heartbeat.payload != PayloadFor(heartbeat.sequence)) + { + return false; + } + const auto crc32 = Crc32Of(heartbeat.sequence, heartbeat.payload); + return crc32.has_value() && (crc32.value() == heartbeat.crc32); +} + +PlatformInfo MakePlatformInfo() noexcept +{ + PlatformInfo info{}; + CopyTo(info.known_good_timestamp, generated::kKnownGoodTimestamp); + info.module_count = static_cast(generated::kModules.size()); + for (std::size_t i = 0U; i < generated::kModules.size(); ++i) + { + CopyTo(info.modules[i].name, generated::kModules[i].name); + CopyTo(info.modules[i].version, generated::kModules[i].version); + } + info.crc32 = Crc32Of(info).value_or(0U); + return info; +} + +bool IsValid(const PlatformInfo& info) noexcept +{ + if ((info.module_count > kMaxModules) || !IsTerminated(info.known_good_timestamp)) + { + return false; + } + for (const auto& module : info.modules) + { + if (!IsTerminated(module.name) || !IsTerminated(module.version)) + { + return false; + } + } + const auto crc32 = Crc32Of(info); + return crc32.has_value() && (crc32.value() == info.crc32); +} + +std::string ToString(const PlatformInfo& info) +{ + std::string text{"known_good "}; + text.append(View(info.known_good_timestamp)); + for (std::size_t i = 0U; i < info.module_count; ++i) + { + text.append("\n ").append(View(info.modules[i].name)).append(" ").append(View(info.modules[i].version)); + } + return text; +} + +std::string Compare(const PlatformInfo& expected, const PlatformInfo& actual) +{ + std::string differences{}; + if (View(expected.known_good_timestamp) != View(actual.known_good_timestamp)) + { + differences.append("known_good: expected ") + .append(View(expected.known_good_timestamp)) + .append(", platform has ") + .append(View(actual.known_good_timestamp)) + .append("\n"); + } + for (std::size_t i = 0U; i < expected.module_count; ++i) + { + const auto& wanted = expected.modules[i]; + const auto* found = FindModule(actual, View(wanted.name)); + if (found == nullptr) + { + differences.append(View(wanted.name)).append(": missing on the platform\n"); + } + else if (View(found->version) != View(wanted.version)) + { + differences.append(View(wanted.name)) + .append(": expected ") + .append(View(wanted.version)) + .append(", platform has ") + .append(View(found->version)) + .append("\n"); + } + } + for (std::size_t i = 0U; i < actual.module_count; ++i) + { + if (FindModule(expected, View(actual.modules[i].name)) == nullptr) + { + differences.append(View(actual.modules[i].name)).append(": not part of the expected S-CORE version\n"); + } + } + return differences; +} + +} // namespace score::platform::heartbeat diff --git a/deploy/communication/heartbeat.h b/deploy/communication/heartbeat.h new file mode 100644 index 00000000000..17ccac6abfd --- /dev/null +++ b/deploy/communication/heartbeat.h @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef DEPLOY_COMMUNICATION_HEARTBEAT_H +#define DEPLOY_COMMUNICATION_HEARTBEAT_H + +#include "score/mw/com/types.h" + +#include +#include +#include +#include + +namespace score::platform::heartbeat +{ + +// Resolved by provider and consumer through the deployed mw_com_config.json. +inline constexpr std::string_view kInstanceSpecifier{"score/platform/heartbeat"}; + +inline constexpr std::size_t kPayloadSize{32U}; + +struct Heartbeat +{ + std::uint64_t sequence; + std::array payload; + std::uint32_t crc32; +}; + +inline constexpr std::size_t kMaxModules{16U}; +inline constexpr std::size_t kNameSize{40U}; +inline constexpr std::size_t kVersionSize{48U}; +inline constexpr std::size_t kTimestampSize{32U}; + +// Fixed-size, zero-terminated strings: the value lives in shared memory. +struct ModuleVersion +{ + std::array name; + std::array version; +}; + +/// The S-CORE version the platform was built from: the known_good.json snapshot and its target_sw modules. +struct PlatformInfo +{ + std::array known_good_timestamp; + std::uint32_t module_count; + std::array modules; + std::uint32_t crc32; +}; + +template +class HeartbeatInterface : public Trait::Base +{ + public: + using Trait::Base::Base; + + typename Trait::template Event heartbeat_{*this, "heartbeat"}; + typename Trait::template Field platform_info_{*this, "platform_info"}; +}; + +using HeartbeatSkeleton = score::mw::com::AsSkeleton; +using HeartbeatProxy = score::mw::com::AsProxy; + +/// Heartbeat number `sequence`: a payload derived from the sequence, sealed with a baselibs CRC32. +Heartbeat MakeHeartbeat(std::uint64_t sequence) noexcept; + +/// True if the payload matches the sequence and the baselibs CRC32 over both is intact. +bool IsValid(const Heartbeat& heartbeat) noexcept; + +/// The S-CORE version this binary was built from, sealed with a baselibs CRC32. +PlatformInfo MakePlatformInfo() noexcept; + +/// True if the module count is in range, all strings are terminated and the baselibs CRC32 is intact. +bool IsValid(const PlatformInfo& info) noexcept; + +/// One line per module, e.g. "score_baselibs 0.2.16". +std::string ToString(const PlatformInfo& info); + +/// Differences between `expected` and `actual`, one per line; empty if both describe the same version. +std::string Compare(const PlatformInfo& expected, const PlatformInfo& actual); + +} // namespace score::platform::heartbeat + +#endif // DEPLOY_COMMUNICATION_HEARTBEAT_H diff --git a/deploy/communication/heartbeat_service.cpp b/deploy/communication/heartbeat_service.cpp new file mode 100644 index 00000000000..cf78dc870c5 --- /dev/null +++ b/deploy/communication/heartbeat_service.cpp @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// Platform heartbeat service: offers the score/platform/heartbeat mw::com service, +// reports the S-CORE version the platform was built from in the platform_info field +// and publishes one heartbeat per cycle until SIGTERM or SIGINT. + +#include "deploy/communication/heartbeat.h" + +#include "score/mw/com/runtime.h" +#include "score/string_manipulation/arguments/arguments.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +constexpr std::chrono::milliseconds kCycle{100}; + +volatile std::sig_atomic_t g_stop_requested{0}; + +extern "C" void RequestStop(int /*signal*/) +{ + g_stop_requested = 1; +} + +} // namespace + +int main(int argc, const char* argv[]) +{ + using namespace score::platform::heartbeat; + + std::signal(SIGTERM, RequestStop); + std::signal(SIGINT, RequestStop); + + score::mw::com::runtime::InitializeRuntime(score::string_manipulation::GetArguments(argc, argv)); + + auto instance_specifier = score::mw::com::InstanceSpecifier::Create(std::string{kInstanceSpecifier}); + if (!instance_specifier.has_value()) + { + std::cerr << "heartbeat_service: invalid instance specifier: " << std::move(instance_specifier).error() + << '\n'; + return 1; + } + + auto skeleton = HeartbeatSkeleton::Create(std::move(instance_specifier).value()); + if (!skeleton.has_value()) + { + std::cerr << "heartbeat_service: cannot create skeleton: " << std::move(skeleton).error() << '\n'; + return 2; + } + + const auto platform_info = MakePlatformInfo(); + const auto published = skeleton->platform_info_.Update(platform_info); + if (!published.has_value()) + { + std::cerr << "heartbeat_service: cannot publish the platform info: " << published.error() << '\n'; + return 4; + } + + const auto offered = skeleton->OfferService(); + if (!offered.has_value()) + { + std::cerr << "heartbeat_service: cannot offer service: " << offered.error() << '\n'; + return 3; + } + std::cout << "heartbeat_service: offering " << kInstanceSpecifier << " for S-CORE " + << ToString(platform_info) << std::endl; + + std::uint64_t sequence{0U}; + while (g_stop_requested == 0) + { + const auto sent = skeleton->heartbeat_.Send(MakeHeartbeat(sequence)); + if (!sent.has_value()) + { + std::cerr << "heartbeat_service: send failed for heartbeat " << sequence << ": " << sent.error() << '\n'; + } + ++sequence; + std::this_thread::sleep_for(kCycle); + } + + skeleton->StopOfferService(); + std::cout << "heartbeat_service: stopped after " << sequence << " heartbeats" << std::endl; + return 0; +} diff --git a/platform_integration_tests/communication/test_communication_showcase.py b/platform_integration_tests/communication/test_communication_showcase.py index 0ccd0f6e3da..2ce584de75f 100644 --- a/platform_integration_tests/communication/test_communication_showcase.py +++ b/platform_integration_tests/communication/test_communication_showcase.py @@ -44,3 +44,23 @@ def test_communication_showcase(target): platform.""" run_showcases(target, "Communication Sender Receiver Example") + + +@add_test_properties( + partially_verifies=["stkh_req__communication__inter_process"], + test_type="interface-test", + derivation_technique="requirements-analysis", +) +def test_communication_platform_heartbeat(target): + """A consumer process finds the heartbeat service deployed by the platform + through mw::com service discovery, reads the S-CORE version the platform + reports and receives heartbeats from it whose baselibs CRC32 is intact. + The consumer brings no provider of its own, so this fails if the platform + does not deploy a working service or runs a different S-CORE version.""" + + output = run_showcases(target, "Communication Platform Heartbeat") + assert "heartbeat_consumer: platform runs S-CORE" in output, ( + f"Platform did not report its S-CORE version:\n{output}" + ) + assert "heartbeat_consumer: received" in output, f"Consumer did not receive heartbeats from the platform:\n{output}" + assert "heartbeat_consumer: FAILED" not in output, output diff --git a/platform_integration_tests/showcase_runner.py b/platform_integration_tests/showcase_runner.py index 9b8ced60119..2e4898d2895 100644 --- a/platform_integration_tests/showcase_runner.py +++ b/platform_integration_tests/showcase_runner.py @@ -19,13 +19,14 @@ _CLI = "/showcases/bin/cli" -def run_showcases(target, *names: str) -> None: +def run_showcases(target, *names: str) -> str: """Run the named showcases via the showcase entrypoint and assert success. ``run_score`` in the showcase CLI prints a per-example completion banner and exits 0 once every app of an example has been spawned and joined, so both the exit code and the banner are checked to confirm the showcase ran - end-to-end on the platform. + end-to-end on the platform. The CLI ignores the apps' exit codes, so the + combined output is returned for app-specific checks. """ selection = ",".join(names) exit_code, out = target.execute(f"{_CLI} --examples='{selection}'") @@ -36,3 +37,4 @@ def run_showcases(target, *names: str) -> None: assert f"Example '{name}' finished successfully" in output, ( f"Showcase '{name}' did not finish successfully:\n{output}" ) + return output diff --git a/showcases/standalone/BUILD b/showcases/standalone/BUILD index 4252ecb33e9..c7e4adbfee0 100644 --- a/showcases/standalone/BUILD +++ b/showcases/standalone/BUILD @@ -10,13 +10,30 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_binary") load("//bazel_common:bundlers.bzl", "score_pkg_bundle") +# Consumer of the heartbeat service deployed by //deploy/communication. It +# brings no provider of its own, so it fails if the platform does not deploy one. +cc_binary( + name = "heartbeat_consumer", + srcs = ["heartbeat_consumer.cpp"], + deps = [ + "//deploy/communication:heartbeat_interface", + "@score_baselibs//score/string_manipulation/arguments", + "@score_communication//score/mw/com", + ], +) + score_pkg_bundle( name = "comm", - bins = ["@score_communication//score/mw/com/example/com-api-example:com-api-example"], + bins = [ + "@score_communication//score/mw/com/example/com-api-example:com-api-example", + "//showcases/standalone:heartbeat_consumer", + ], config_data = [ "//showcases/standalone:com.score.json", + "//showcases/standalone:com_heartbeat.score.json", ], custom_layout = { "@score_communication//score/mw/com/example/com-api-example:etc/mw_com_config.json": "etc/mw_com_config.json", diff --git a/showcases/standalone/com_heartbeat.score.json b/showcases/standalone/com_heartbeat.score.json new file mode 100644 index 00000000000..7a88a301acb --- /dev/null +++ b/showcases/standalone/com_heartbeat.score.json @@ -0,0 +1,24 @@ +[ + { + "name": "Communication Platform Heartbeat", + "description": "A consumer must find the heartbeat service deployed by the platform and receive CRC32-checked heartbeats from it", + "apps": [ + { + "path": "/bin/sh", + "args": [ + "-c", + "cd /usr/bin/communication && ./heartbeat_service >/dev/null 2>&1 &" + ], + "env": {} + }, + { + "path": "/showcases/bin/heartbeat_consumer", + "args": [ + "--service_instance_manifest", + "/usr/bin/communication/etc/mw_com_config.json" + ], + "env": {} + } + ] + } +] diff --git a/showcases/standalone/heartbeat_consumer.cpp b/showcases/standalone/heartbeat_consumer.cpp new file mode 100644 index 00000000000..a6244e37921 --- /dev/null +++ b/showcases/standalone/heartbeat_consumer.cpp @@ -0,0 +1,177 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// Consumer of the platform heartbeat service (//deploy/communication). +// +// Exits 0 only if it finds the deployed service via mw::com service discovery, +// the platform reports the S-CORE version this consumer was built for, and it +// receives kExpectedHeartbeats heartbeats whose baselibs CRC32 is intact. + +#include "deploy/communication/heartbeat.h" + +#include "score/mw/com/runtime.h" +#include "score/string_manipulation/arguments/arguments.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using namespace std::chrono_literals; +using score::platform::heartbeat::Heartbeat; +using score::platform::heartbeat::HeartbeatProxy; +using score::platform::heartbeat::PlatformInfo; + +constexpr auto kDiscoveryTimeout{10s}; +constexpr auto kReceiveTimeout{10s}; +constexpr auto kPollInterval{50ms}; +constexpr std::size_t kMaxSamples{5U}; +constexpr std::size_t kExpectedHeartbeats{10U}; + +int Fail(const std::string& reason) +{ + std::cerr << "heartbeat_consumer: FAILED - " << reason << std::endl; + return 1; +} + +std::optional ReceivePlatformInfo(HeartbeatProxy& proxy) +{ + if (!proxy.platform_info_.Subscribe(1U).has_value()) + { + return std::nullopt; + } + std::optional info{}; + const auto deadline = std::chrono::steady_clock::now() + kReceiveTimeout; + while (!info.has_value() && (std::chrono::steady_clock::now() < deadline)) + { + std::ignore = proxy.platform_info_.GetNewSamples( + [&info](score::mw::com::SamplePtr sample) noexcept { + info = *sample; + }, + 1U); + if (!info.has_value()) + { + std::this_thread::sleep_for(kPollInterval); + } + } + proxy.platform_info_.Unsubscribe(); + return info; +} + +} // namespace + +int main(int argc, const char* argv[]) +{ + score::mw::com::runtime::InitializeRuntime(score::string_manipulation::GetArguments(argc, argv)); + + auto instance_specifier = + score::mw::com::InstanceSpecifier::Create(std::string{score::platform::heartbeat::kInstanceSpecifier}); + if (!instance_specifier.has_value()) + { + return Fail("invalid instance specifier"); + } + + score::mw::com::ServiceHandleContainer handles{}; + const auto discovery_deadline = std::chrono::steady_clock::now() + kDiscoveryTimeout; + while (handles.empty() && (std::chrono::steady_clock::now() < discovery_deadline)) + { + auto found = HeartbeatProxy::FindService(instance_specifier.value()); + if (found.has_value()) + { + handles = std::move(found).value(); + } + if (handles.empty()) + { + std::this_thread::sleep_for(kPollInterval); + } + } + if (handles.empty()) + { + return Fail("the platform offers no heartbeat service (is //deploy/communication deployed and running?)"); + } + std::cout << "heartbeat_consumer: found the platform heartbeat service" << std::endl; + + auto proxy = HeartbeatProxy::Create(handles.front()); + if (!proxy.has_value()) + { + return Fail("cannot connect to the heartbeat service"); + } + + const auto platform_info = ReceivePlatformInfo(proxy.value()); + if (!platform_info.has_value()) + { + return Fail("the platform reports no S-CORE version"); + } + if (!score::platform::heartbeat::IsValid(platform_info.value())) + { + return Fail("the S-CORE version reported by the platform fails the CRC32 check"); + } + std::cout << "heartbeat_consumer: platform runs S-CORE " + << score::platform::heartbeat::ToString(platform_info.value()) << std::endl; + const auto differences = + score::platform::heartbeat::Compare(score::platform::heartbeat::MakePlatformInfo(), platform_info.value()); + if (!differences.empty()) + { + return Fail("the platform runs a different S-CORE version than this check was built for:\n" + differences); + } + + if (!proxy->heartbeat_.Subscribe(kMaxSamples).has_value()) + { + return Fail("cannot subscribe to heartbeats"); + } + + std::size_t valid{0U}; + std::size_t corrupt{0U}; + std::uint64_t last_sequence{0U}; + const auto receive_deadline = std::chrono::steady_clock::now() + kReceiveTimeout; + while ((valid < kExpectedHeartbeats) && (corrupt == 0U) && + (std::chrono::steady_clock::now() < receive_deadline)) + { + std::ignore = proxy->heartbeat_.GetNewSamples( + [&valid, &corrupt, &last_sequence](score::mw::com::SamplePtr sample) noexcept { + if (score::platform::heartbeat::IsValid(*sample)) + { + ++valid; + last_sequence = sample->sequence; + } + else + { + ++corrupt; + } + }, + kMaxSamples); + std::this_thread::sleep_for(kPollInterval); + } + + if (corrupt != 0U) + { + return Fail("received " + std::to_string(corrupt) + " heartbeat(s) failing the CRC32 check"); + } + if (valid < kExpectedHeartbeats) + { + return Fail("received only " + std::to_string(valid) + " of " + std::to_string(kExpectedHeartbeats) + + " heartbeats"); + } + + std::cout << "heartbeat_consumer: received " << valid + << " valid heartbeats from the platform heartbeat service (last sequence " << last_sequence << ")" + << std::endl; + return 0; +} From 68c4e2b632ad6a77431e1c9bec2ed0d5fd097873 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 30 Oct 2026 10:39:26 +0000 Subject: [PATCH 7/8] Publish platform and showcases as separate release downloads Includes //deploy:platform_tar in the AutoSD image, which so far only took the showcases. Since the launch manager, config daemon and heartbeat service moved into //deploy, the AutoSD showcases could not find the binaries they drive ("config_daemon: No such file or directory"). On a release, the Linux, QNX and EB corbos workflows now attach two archives per target: score-platform-.tar.gz (//deploy:platform_tar, layer 2) and score-showcases-.tar.gz (//showcases:showcases_pkg_tar, layer 3). An image that claims to ship S-CORE can then be checked by copying only the showcases archive onto it. deploy/README.md lists the assets and how to use them. --- .github/workflows/build_and_test_ebclfsa.yml | 35 ++++++++++++++++++ .github/workflows/build_and_test_linux.yml | 12 +++++++ .github/workflows/build_and_test_qnx.yml | 38 ++++++++++++++++++++ deploy/README.md | 28 +++++++++++++++ images/autosd/BUILD.bazel | 1 + 5 files changed, 114 insertions(+) diff --git a/.github/workflows/build_and_test_ebclfsa.yml b/.github/workflows/build_and_test_ebclfsa.yml index fe6bf99eaa9..343bb2c4819 100644 --- a/.github/workflows/build_and_test_ebclfsa.yml +++ b/.github/workflows/build_and_test_ebclfsa.yml @@ -12,6 +12,8 @@ # ******************************************************************************* name: EB corbos Linux for Safety Applications - Build & Integration Test on: + release: + types: [created] pull_request: types: [opened, reopened, synchronize] merge_group: @@ -50,3 +52,36 @@ jobs: name: test-logs path: | bazel-bin/images/ebclfsa_aarch64/build/*.log + - name: Package platform and showcases + if: github.event_name == 'release' + run: | + bazel build --lockfile_mode=error --config=eb-aarch64 //deploy:platform_tar //showcases:showcases_pkg_tar + gzip -c bazel-bin/deploy/platform_tar.tar > score-platform-eb-aarch64.tar.gz + gzip -c bazel-bin/showcases/showcases_pkg_tar.tar > score-showcases-eb-aarch64.tar.gz + - name: Store platform and showcases packages + if: github.event_name == 'release' + uses: actions/upload-artifact@v4 + with: + name: score-release-eb-aarch64 + path: | + score-platform-eb-aarch64.tar.gz + score-showcases-eb-aarch64.tar.gz + release: + name: Publish platform and showcases to release + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Download platform and showcases packages + uses: actions/download-artifact@v4 + with: + name: score-release-eb-aarch64 + path: ./release-assets + - name: Upload platform and showcases to release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$RELEASE_TAG" ./release-assets/*.tar.gz --clobber diff --git a/.github/workflows/build_and_test_linux.yml b/.github/workflows/build_and_test_linux.yml index 16645c80720..1e46fb81e5a 100644 --- a/.github/workflows/build_and_test_linux.yml +++ b/.github/workflows/build_and_test_linux.yml @@ -59,3 +59,15 @@ jobs: with: name: sbom-spdx path: bazel-bin/sbom.spdx.json + - name: Package platform and showcases + if: github.event_name == 'release' + run: | + bazel build --lockfile_mode=error --config=linux-x86_64 //deploy:platform_tar //showcases:showcases_pkg_tar + gzip -c bazel-bin/deploy/platform_tar.tar > score-platform-linux-x86_64.tar.gz + gzip -c bazel-bin/showcases/showcases_pkg_tar.tar > score-showcases-linux-x86_64.tar.gz + - name: Upload platform and showcases to release + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$RELEASE_TAG" score-platform-linux-x86_64.tar.gz score-showcases-linux-x86_64.tar.gz --clobber diff --git a/.github/workflows/build_and_test_qnx.yml b/.github/workflows/build_and_test_qnx.yml index ab881069054..8e27c4b998d 100644 --- a/.github/workflows/build_and_test_qnx.yml +++ b/.github/workflows/build_and_test_qnx.yml @@ -12,6 +12,8 @@ # ******************************************************************************* name: QNX8 - Build & Integration Test on: + release: + types: [created] pull_request_target: types: [opened, reopened, synchronize] merge_group: @@ -80,6 +82,42 @@ jobs: - name: Run platform integration tests run: | bazel test --lockfile_mode=error --config=itf-qnx-x86_64 //platform_integration_tests:pit + - name: Package platform and showcases + if: github.event_name == 'release' + env: + SCORE_QNX_USER: ${{ secrets.SCORE_QNX_USER }} + SCORE_QNX_PASSWORD: ${{ secrets.SCORE_QNX_PASSWORD }} + run: | + bazel build --lockfile_mode=error --config qnx-x86_64 //deploy:platform_tar //showcases:showcases_pkg_tar + gzip -c bazel-bin/deploy/platform_tar.tar > score-platform-qnx-x86_64.tar.gz + gzip -c bazel-bin/showcases/showcases_pkg_tar.tar > score-showcases-qnx-x86_64.tar.gz + - name: Store platform and showcases packages + if: github.event_name == 'release' + uses: actions/upload-artifact@v4 + with: + name: score-release-qnx-x86_64 + path: | + score-platform-qnx-x86_64.tar.gz + score-showcases-qnx-x86_64.tar.gz - name: Cleanup QNX license if: always() run: sudo rm -rf /opt/score_qnx + release: + name: Publish platform and showcases to release + needs: qnx-build-x86_64 + if: github.event_name == 'release' + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Download platform and showcases packages + uses: actions/download-artifact@v4 + with: + name: score-release-qnx-x86_64 + path: ./release-assets + - name: Upload platform and showcases to release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$RELEASE_TAG" ./release-assets/*.tar.gz --clobber diff --git a/deploy/README.md b/deploy/README.md index 9907b881918..e206effe990 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -69,3 +69,31 @@ that service and fails unless the platform provides it. Long term each S-CORE module should export its own deployment package upstream, so that "deployed correctly" becomes checkable rather than agreed. Until then this directory is the distribution's local stand-in for it. + +## Release downloads: layer 2 and layer 3 separately + +Every GitHub release carries both upper layers as separate downloads, one +pair of archives per target the CI builds: + +| Target | Layer 2 (platform) | Layer 3 (showcases) | Built with | +| --- | --- | --- | --- | +| Linux x86_64 | `score-platform-linux-x86_64.tar.gz` | `score-showcases-linux-x86_64.tar.gz` | `--config=linux-x86_64` | +| AutoSD x86_64 | `score-platform-autosd-x86_64.tar.gz` | `score-showcases-autosd-x86_64.tar.gz` | `--config=autosd-x86_64` | +| QNX x86_64 | `score-platform-qnx-x86_64.tar.gz` | `score-showcases-qnx-x86_64.tar.gz` | `--config=qnx-x86_64` | +| EB corbos aarch64 | `score-platform-eb-aarch64.tar.gz` | `score-showcases-eb-aarch64.tar.gz` | `--config=eb-aarch64` | + +The platform archive is `//deploy:platform_tar` (unpacks to `/usr/bin/...`), +the showcases archive is `//showcases:showcases_pkg_tar` (unpacks to +`/showcases`). Both are relative to `/`: + +```sh +tar -xzf score-platform-.tar.gz -C / # only if the image does not ship S-CORE itself +tar -xzf score-showcases-.tar.gz -C / +/showcases/bin/cli --examples all +``` + +To check an image that claims to ship S-CORE, copy only the showcases +archive onto it. The showcases then run against whatever platform the image +deploys, without rebuilding it — as long as it fulfils the contract above. +`heartbeat_consumer` additionally fails if the platform reports a different +S-CORE version than the release the archive belongs to. diff --git a/images/autosd/BUILD.bazel b/images/autosd/BUILD.bazel index 391599e716e..199e2f0c51c 100644 --- a/images/autosd/BUILD.bazel +++ b/images/autosd/BUILD.bazel @@ -37,6 +37,7 @@ oci_image( name = "image_amd64", base = "@eclipse_autosd10_qemu_amd64", tars = [ + "//deploy:platform_tar", "//showcases:showcases_pkg_tar", ], ) From a29ea002a313cbceeb66c66efcd1ccd7536ad70c Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 30 Oct 2026 10:42:10 +0000 Subject: [PATCH 8/8] AutoSD: attach platform and showcases archives to the release Extends the release job from #413: the build job packages //deploy:platform_tar and //showcases:showcases_pkg_tar for AutoSD, and the release job uploads them next to the qcow2 image. GH_REPO is set because the release job has no checkout to infer the repository from. --- .github/workflows/build_and_test_autosd.yml | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build_and_test_autosd.yml b/.github/workflows/build_and_test_autosd.yml index d49be5b3463..3677564e148 100644 --- a/.github/workflows/build_and_test_autosd.yml +++ b/.github/workflows/build_and_test_autosd.yml @@ -128,6 +128,20 @@ jobs: with: name: eclipse-autosd-bootc-qemu-${{ matrix.platform }}.qcow2.xz path: images/autosd/eclipse-autosd-bootc-qemu-${{ matrix.platform }}.qcow2.xz + - name: Package platform and showcases + if: github.event_name == 'release' + run: | + bazel build --lockfile_mode=error --config autosd-${{ matrix.platform }} //deploy:platform_tar //showcases:showcases_pkg_tar + gzip -c bazel-bin/deploy/platform_tar.tar > score-platform-autosd-${{ matrix.platform }}.tar.gz + gzip -c bazel-bin/showcases/showcases_pkg_tar.tar > score-showcases-autosd-${{ matrix.platform }}.tar.gz + - name: Store platform and showcases packages + if: github.event_name == 'release' + uses: actions/upload-artifact@v4 + with: + name: score-release-autosd-${{ matrix.platform }} + path: | + score-platform-autosd-${{ matrix.platform }}.tar.gz + score-showcases-autosd-${{ matrix.platform }}.tar.gz release: name: Publish image to release needs: build @@ -143,10 +157,18 @@ jobs: with: name: eclipse-autosd-bootc-qemu-x86_64.qcow2.xz path: ./release-assets + - name: Download platform and showcases packages + uses: actions/download-artifact@v4 + with: + name: score-release-autosd-x86_64 + path: ./release-assets - name: Upload image to release env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} run: | gh release upload "$RELEASE_NAME" \ ./release-assets/eclipse-autosd-bootc-qemu-x86_64.qcow2.xz \ + ./release-assets/score-platform-autosd-x86_64.tar.gz \ + ./release-assets/score-showcases-autosd-x86_64.tar.gz \ --clobber