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 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 5bbd3969c91..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: @@ -77,6 +79,45 @@ 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: 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/BUILD b/deploy/BUILD new file mode 100644 index 00000000000..b8e1d72f4b7 --- /dev/null +++ b/deploy/BUILD @@ -0,0 +1,44 @@ +# ******************************************************************************* +# 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: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", + "//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..e206effe990 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,99 @@ +# 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` | +| `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 +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 +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`. 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 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. +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/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/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..6c6930cb356 --- /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 = [ + "etc/log-channels.json", + "etc/logging.json", + ], + attributes = pkg_attributes(mode = "0644"), + prefix = "usr/bin/datarouter/etc", + strip_prefix = strip_prefix.files_only(), +) 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/docs/integration_process/step_4_platforms.rst b/docs/integration_process/step_4_platforms.rst index 75660311801..d61867559f2 100644 --- a/docs/integration_process/step_4_platforms.rst +++ b/docs/integration_process/step_4_platforms.rst @@ -181,39 +181,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"], @@ -223,12 +248,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/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/feature_integration_tests/configs/etc/logging.json b/feature_integration_tests/configs/etc/logging.json deleted file mode 100644 index 8b3d1eacc88..00000000000 --- a/feature_integration_tests/configs/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/feature_integration_tests/configs/etc/mw_com_config.json b/feature_integration_tests/configs/etc/mw_com_config.json deleted file mode 100644 index 1e104bd4843..00000000000 --- a/feature_integration_tests/configs/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/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", ], ) 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/ebclfsa_aarch64/build/BUILD b/images/ebclfsa_aarch64/build/BUILD index b5d66fbafe6..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", - "//feature_integration_tests/configs:etc_configs", ":extract-fastdev-image", ":crinit_config_tar", ":scripts_tar", 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/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/BUILD b/showcases/BUILD index 261f806ca08..9db20cf9b35 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", "//showcases/standalone:baselibs_pkg_files", ], @@ -42,6 +43,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 f6ead072a8e..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", @@ -67,18 +84,29 @@ 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. +# 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", ) 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/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" } ] } 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; +}