Compare commits

..
1 Commits
Author SHA1 Message Date
Anusmita Dutta Mazumder a19f2dba81 arm/linux-yocto: enable /dev/mem for Trusted Services tests
The Trusted Services uefi-test uses /dev/mem to map its
MM communication buffer. The no-strict-devmem.cfg fragment
relaxes the mapping restrictions, while CONFIG_DEVMEM
controls whether /dev/mem is available.

Enable CONFIG_DEVMEM in the same fragment so the test can
open /dev/mem and map the communication buffer.

Signed-off-by: Anusmita Dutta Mazumder <anusmita.duttamazumder@arm.com>
Signed-off-by: Jon Mason <jon.mason@arm.com>
2026-10-22 10:38:56 -04:00
38 changed files with 98 additions and 1910 deletions

No files matched your search

+1 -2
View File
@@ -1,4 +1,4 @@
image: ${MIRROR_GHCR}/siemens/kas/kas:5.5 image: ${MIRROR_GHCR}/siemens/kas/kas:5.2
variables: variables:
# These are needed as the k8s executor doesn't respect the container # These are needed as the k8s executor doesn't respect the container
@@ -284,7 +284,6 @@ qemuarm64:
- DISTRO: poky-tiny - DISTRO: poky-tiny
FIRMWARE: [u-boot, edk2] FIRMWARE: [u-boot, edk2]
TESTING: testimage TESTING: testimage
- TF: tf-rmm
qemuarm-secureboot: qemuarm-secureboot:
extends: .build extends: .build
-7
View File
@@ -1,7 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siemens/kas/master/kas/schema-kas.json
header:
version: 14
target:
- trusted-firmware-rmm
@@ -10,8 +10,8 @@ product: Corstone-1000 with Cortex-A320
product_type: Open Source Projects product_type: Open Source Projects
product_revision: product_revision:
- prefix: Version - prefix: Version
version: "0102" version: "0101"
version_label: "2026.11" version_label: "2026.02"
output_type: "software" output_type: "software"
conformance_notice: "true" conformance_notice: "true"
content_type: User Guide content_type: User Guide
@@ -24,8 +24,8 @@ themes:
intended_audience: Software Developers intended_audience: Software Developers
terminology_review: new terminology_review: new
releases: releases:
- revision: 0102-01 - revision: 0100-01
date: 2026-11-09 date: 2026-05-01
change_summary: Update change_summary: Update
permissions: nonconfidential permissions: nonconfidential
@@ -97,7 +97,7 @@ variables:
- name: secure_debug_manager_armds_integration - name: secure_debug_manager_armds_integration
value: 'https://github.com/ARM-software/secure-debug-manager?tab=readme-ov-file#arm-development-studio-integration' value: 'https://github.com/ARM-software/secure-debug-manager?tab=readme-ov-file#arm-development-studio-integration'
- name: meta_arm_repository_release_branch - name: meta_arm_repository_release_branch
value: 'https://docs.yoctoproject.org/next/migration-guides/migration-6.1.html' value: 'https://docs.yoctoproject.org/next/migration-guides/migration-6.0.html'
- name: arm_ulink_pro_website - name: arm_ulink_pro_website
value: 'https://www.arm.com/products/development-tools/debug-probes/ulink-pro' value: 'https://www.arm.com/products/development-tools/debug-probes/ulink-pro'
- name: arm_ds_website - name: arm_ds_website
@@ -2,73 +2,6 @@
This document contains a summary of the new features, changes and fixes in each release of the Corstone-1000 with Cortex-A320 software stack. This document contains a summary of the new features, changes and fixes in each release of the Corstone-1000 with Cortex-A320 software stack.
## Version 2026.11 {.reference}
The following changes are present in this release:
- Continued Corstone-1000 with Cortex-A320 enablement with U-Boot 2026.07, TF-A 2.15.0, TF-M 2.3.1, OP-TEE 4.10.0, Trusted Services 1.3.0, and Linux 6.19.
- Updated TF-M to include Corstone-1000 security fixes and the latest firmware-update partition fixes.
### Corstone-1000 with Cortex-A320 components versions {.reference}
The following component versions are available:
Table: Corstone-1000 with Cortex-A320 component versions
+----------------------------------------+-----------------------------------+
| Component | Version |
+========================================+===================================+
| linux-yocto | 6.19 |
+----------------------------------------+-----------------------------------+
| u-boot | 2026.07 |
+----------------------------------------+-----------------------------------+
| optee-client | 4.10.0 |
+----------------------------------------+-----------------------------------+
| optee-os | 4.10.0 |
+----------------------------------------+-----------------------------------+
| trusted-firmware-a | 2.15.0 |
+----------------------------------------+-----------------------------------+
| trusted-firmware-m | 2.3.1 |
+----------------------------------------+-----------------------------------+
| libts | v1.3.0 |
+----------------------------------------+-----------------------------------+
| ts-sp-{se-proxy, smm-gateway} | v1.3.0 |
+----------------------------------------+-----------------------------------+
| ts-psa-{crypto, iat, its. ps}-api-test | 74dc6646ff |
+----------------------------------------+-----------------------------------+
### Yocto distribution components versions {.reference}
The following Yocto distribution components versions are available:
Table: Yocto distribution component versions
+-------------------+------------+
| Component | Version |
+===================+============+
| meta-arm | blacksail |
+-------------------+------------+
| bitbake | 634f8dede6 |
+-------------------+------------+
| meta-openembedded | a9d69bc066 |
+-------------------+------------+
| openembedded-core | e182a0c4b9 |
+-------------------+------------+
| meta-yocto | d0119aa2eb |
+-------------------+------------+
| meta-secure-core | 56316bacdb |
+-------------------+------------+
| busybox | 1.38.0 |
+-------------------+------------+
| musl | 1.2.6 |
+-------------------+------------+
| gcc-arm-none-eabi | 15.3.rel1 |
+-------------------+------------+
| gcc-cross-aarch64 | 16.2.0 |
+-------------------+------------+
| openssl | 4.0.3 |
+-------------------+------------+
## Version 2026.05 {.reference} ## Version 2026.05 {.reference}
The following changes are present in this release: The following changes are present in this release:
@@ -7,15 +7,6 @@ intended for safety-critical applications. Should your software or hardware
prove defective, you assume the entire cost of all necessary servicing, repair prove defective, you assume the entire cost of all necessary servicing, repair
or correction. or correction.
## Release notes - 2026.11 {.reference}
The following knowns issues and limitations are present in this release:
- Crypto isolation is not supported in the Secure world of Corstone-1000 with Cortex-A320. Additionally, clients in
the Normal world are not isolated from one another. Therefore, if an end user wants to add a new
Secure Partition (SP) (such as a software TPM) that accesses the Crypto service via the SE-Proxy,
they are responsible for implementing their own isolation mechanisms to ensure proper security boundaries.
## Release notes - 2026.05 {.reference} ## Release notes - 2026.05 {.reference}
The following knowns issues and limitations are present in this release: The following knowns issues and limitations are present in this release:
@@ -4,7 +4,7 @@ All the tests in this chapter assume you have already built the software stack a
## Reports {.reference} ## Reports {.reference}
Reports for the tests conducted on the [Corstone-1000 software (CORSTONE1000-2026.11)](https://git.yoctoproject.org/meta-arm/tag/?h=CORSTONE1000-2026.11) release are available for reference in the [GitLab repo](https://gitlab.arm.com/arm-reference-solutions/arm-reference-solutions-test-report/-/tree/CORSTONE1000-2026.11/embedded-a/corstone1000/CORSTONE1000-2026.11?ref_type=tags). Reports for the tests conducted on the [Corstone-1000 software (CORSTONE1000-2026.05)](https://git.yoctoproject.org/meta-arm/tag/?h=CORSTONE1000-2026.05) release are available for reference in the [GitLab repo](https://gitlab.arm.com/arm-reference-solutions/arm-reference-solutions-test-report/-/tree/CORSTONE1000-2026.05/embedded-a/corstone1000/CORSTONE1000-2026.05?ref_type=tags).
## SystemReady IR {.reference} ## SystemReady IR {.reference}
@@ -29,8 +29,8 @@ A storage with EFI System Partition (ESP) must exist in the system for the UEFI-
### Use the EFI system partition {.reference} ### Use the EFI system partition {.reference}
The ESP disk image will automatically be used by the Corstone-1000 with Cortex-A320 FVP as the 2nd MMC card image. The ESP disk image will automatically be used by the Corstone-1000 FVP as the 2nd MMC card image.
It will be used when the SystemReady-IR tests are performed on the FVP in the later section. It will be used when the SystemReady-IR tests is performed on the FVP in the later section.
### Run SystemReady IR ACS tests {.reference} ### Run SystemReady IR ACS tests {.reference}
@@ -85,7 +85,7 @@ The `BOOT` partition is also used to store test results in the `acs_results` fol
Ensure that the `acs_results` folder is empty before starting the test. Ensure that the `acs_results` folder is empty before starting the test.
::: :::
To build and run ACS tests on Corstone-1000 with Cortex-A320: To build and run ACS tests on Corstone-1000:
1. On your host development machine, clone the [Arm SystemReady ACS repository](https://github.com/ARM-software/arm-systemready/). 1. On your host development machine, clone the [Arm SystemReady ACS repository](https://github.com/ARM-software/arm-systemready/).
@@ -100,7 +100,7 @@ To build and run ACS tests on Corstone-1000 with Cortex-A320:
2. Find the pre-built ACS live image in `${WORKSPACE}/arm-systemready/IR/prebuilt_images/v23.09_2.1.0/ir-acs-live-image-generic-arm64.wic.xz`. 2. Find the pre-built ACS live image in `${WORKSPACE}/arm-systemready/IR/prebuilt_images/v23.09_2.1.0/ir-acs-live-image-generic-arm64.wic.xz`.
:::note :::note
This prebuilt ACS image includes v5.13 kernel, which does not provide USB driver support for Corstone-1000 with Cortex-A320. This prebuilt ACS image includes v5.13 kernel, which does not provide USB driver support for Corstone-1000.
::: :::
3. Decompress the pre-built ACS live image. 3. Decompress the pre-built ACS live image.
@@ -172,15 +172,14 @@ Table: Payload GUIDs
+-----------+--------------------------------------+ +-----------+--------------------------------------+
The following section describes the steps to update the firmware using Capsule Update The following section describes the steps to update the firmware using Capsule Update
as the Corstone-1000 with Cortex-A320 supports UEFI. as the Corstone-1000 supports UEFI.
The firmware update process is tested with an invalid capsule and with valid capsules to validate the robustness and The firmware update process is tested with an invalid capsule and with valid capsules to validate the robustness and
error-handling capabilities of the firmware update mechanism. error-handling capabilities of the firmware update mechanism.
- Positive full capsule update test: The Corstone-1000 with Cortex-A320 is provided with a valid full capsule, which it applies successfully. The system then boots normally and reaches the Linux command prompt. - Positive full capsule update test: The Corstone-1000 is provided with a valid full capsule, which it applies successfully. The system then boots normally and reaches the Linux command prompt.
- Positive partial capsule update test: The Corstone-1000 with Cortex-A320 is provided with a valid partial capsule that specifies an update for a single component only. The capsule is applied successfully, - Positive partial capsule update test: The Corstone-1000 is provided with a valid partial capsule that specifies an update for a single component only. The capsule is applied successfully, after which the system boots normally and reaches the Linux command prompt.
after which the system boots normally and reaches the Linux command prompt. - Rollback protection capsule update test: The Corstone-1000 is provided with an outdated capsule containing lower version numbers for all payloads. The capsule is correctly rejected due to rollback protection, and the previously installed firmware is retained.
- Rollback protection capsule update test: The Corstone-1000 with Cortex-A320 is provided with an outdated capsule containing lower version numbers for all payloads. The capsule is correctly rejected due to rollback protection, and the previously installed firmware is retained.
Three different capsules are therefore needed to perform the tests. Three different capsules are therefore needed to perform the tests.
@@ -384,7 +383,7 @@ To run the test:
... ...
``` ```
6. Run the following commands in order to run the Corstone-1000 with Cortex-A320 Linux kernel. 6. Run the following commands in order to run the Corstone-1000 Linux kernel.
:::note :::note
Otherwise, the execution ends up in the ACS live image. Otherwise, the execution ends up in the ACS live image.
@@ -421,7 +420,7 @@ To run the test:
Hit any key to stop autoboot: Hit any key to stop autoboot:
``` ```
9. Run the following commands to boot the Corstone-1000 with Cortex-A320 Linux kernel. 9. Run the following commands to boot the Corstone-1000 Linux kernel.
:::note :::note
If these commands are not executed, the system will default to booting into the ACS live image. If these commands are not executed, the system will default to booting into the ACS live image.
@@ -525,7 +524,7 @@ To run the rollback protection capsule update test:
Hit any key to stop autoboot: Hit any key to stop autoboot:
``` ```
8. Run the following commands in order to run the Corstone-1000 with Cortex-A320 Linux kernel, otherwise, the execution ends up in the ACS live image. 8. Run the following commands in order to run the Corstone-1000 Linux kernel, otherwise, the execution ends up in the ACS live image.
``` ```
$ unzip $kernel_addr 0x90000000 $ unzip $kernel_addr 0x90000000
@@ -546,7 +545,7 @@ To run the rollback protection capsule update test:
Hit any key to stop autoboot: Hit any key to stop autoboot:
``` ```
11. Run the following commands to boot the Corstone-1000 with Cortex-A320 Linux kernel. If these commands are not executed, the system will default to booting into the ACS live image. 11. Run the following commands to boot the Corstone-1000 Linux kernel. If these commands are not executed, the system will default to booting into the ACS live image.
``` ```
$ unzip $kernel_addr 0x90000000 $ unzip $kernel_addr 0x90000000
@@ -622,28 +621,28 @@ cat /sys/firmware/efi/esrt/entries/entry0/last_attempt_status
The following list shows the details of the first four ESRT entries for the positive capsule update test: The following list shows the details of the first four ESRT entries for the positive capsule update test:
- BL2 (`${BL2_GUID}`): - BL2 (`${BL2_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- TFM_S (`${TFM_S_GUID}`): - TFM_S (`${TFM_S_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- FIP (`${FIP_GUID}`): - FIP (`${FIP_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- INITRAMFS (`${INITRAMFS_GUID}`): - INITRAMFS (`${INITRAMFS_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
@@ -655,28 +654,28 @@ The following list shows the details of the first four ESRT entries for the posi
The following list shows the details of the first four ESRT entries for the positive partial capsule update test: The following list shows the details of the first four ESRT entries for the positive partial capsule update test:
- BL2 (`${BL2_GUID}`): - BL2 (`${BL2_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 7 - FW version: 7
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 7 - Last attempt version: 7
- TFM_S (`${TFM_S_GUID}`): - TFM_S (`${TFM_S_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- FIP (`${FIP_GUID}`): - FIP (`${FIP_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- INITRAMFS (`${INITRAMFS_GUID}`): - INITRAMFS (`${INITRAMFS_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
@@ -688,28 +687,28 @@ The following list shows the details of the first four ESRT entries for the posi
The following list shows the details of the first four ESRT entries for the rollback protection capsule update test: The following list shows the details of the first four ESRT entries for the rollback protection capsule update test:
- BL2 (`${BL2_GUID}`): - BL2 (`${BL2_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 7 - FW version: 7
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 1 - Last attempt status: 1
- Last attempt version: 5 - Last attempt version: 5
- TFM_S (`${TFM_S_GUID}`): - TFM_S (`${TFM_S_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- FIP (`${FIP_GUID}`): - FIP (`${FIP_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
- Last attempt status: 0 - Last attempt status: 0
- Last attempt version: 6 - Last attempt version: 6
- INITRAMFS (`${INITRAMFS_GUID}`): - INITRAMFS (`${INITRAMFS_GUID}`):
- FW type: 1 - FW type: 0
- FW version: 6 - FW version: 6
- Lowest supported version: 0 - Lowest supported version: 0
- Capsule flags: 0 - Capsule flags: 0
@@ -720,14 +719,14 @@ See the [UEFI documentation](https://uefi.org/specs/UEFI/2.10/23_Firmware_Update
## Linux distributions {.reference} ## Linux distributions {.reference}
This section describes the steps to install major Linux distributions to the Corstone-1000 with Cortex-A320 Host Processor. This sections describes the steps to install major Linux distributions to the Corstone-1000 Host Processor.
The Linux distributions to be installed are: The Linux distributions to be installed are:
- [Debian](https://www.debian.org/) - [Debian](https://www.debian.org/)
- [openSUSE](https://www.opensuse.org/) - [openSUSE](https://www.opensuse.org/)
Follow the instructions below to install the Linux distributions to the Corstone-1000 with Cortex-A320 software stack. Follow the instructions below to install the Linux distributions to the Corstone-1000 software stack.
### Prepare installation media {.reference} ### Prepare installation media {.reference}
@@ -742,7 +741,7 @@ Follow the instructions below to create the installation media.
Note that the location of the ISO file on the development machine will be referred to as `${DISTRO_INSTALLER_ISO_PATH}`. Note that the location of the ISO file on the development machine will be referred to as `${DISTRO_INSTALLER_ISO_PATH}`.
2. Create the installation media which will contain the necessary files to install the operating system. 2. Create the installation media which will contain the necessary files to install the operation system.
The distribution installer ISO file does not need to be burnt to a USB drive. The distribution installer ISO file does not need to be burnt to a USB drive.
It will be used as is when starting the FVP install the distribution. It will be used as is when starting the FVP install the distribution.
@@ -750,9 +749,9 @@ Follow the instructions below to create the installation media.
### Prepare system drive {.reference} ### Prepare system drive {.reference}
A system (or boot) drive, to store all the operating system files and used to boot the distribution, is required as A system (or boot) drive, to store all the operating system files and used to boot the distribution, is required as
Corstone-1000 with Cortex-A320 on-board non-volatile storage size is insufficient for installing the distributions. Corstone-1000 on-board non-volatile storage size is insufficient for installing the distributions.
1. Create a 10 GB GUID Partition Table (GPT) formatted MultiMediaCard (MMC) image. 1. Create an 10 GB GUID Partition Table (GPT) formatted MultiMediaCard (MMC) image.
``` ```
dd if=/dev/zero of=${WORKSPACE}/fvp_distro_system_drive.img \ dd if=/dev/zero of=${WORKSPACE}/fvp_distro_system_drive.img \
@@ -915,7 +914,7 @@ To generate keys:
cd ${WORKSPACE} cd ${WORKSPACE}
git clone https://gitlab.arm.com/arm-reference-solutions/iot-platform-assets \ git clone https://gitlab.arm.com/arm-reference-solutions/iot-platform-assets \
-b CORSTONE1000-2026.11 -b CORSTONE1000-2026.05
``` ```
3. Set the current working directory to build directory's subdirectory containing the software stack build images. 3. Set the current working directory to build directory's subdirectory containing the software stack build images.
@@ -928,7 +927,7 @@ To generate keys:
``` ```
${WORKSPACE}/iot-platform-assets/corstone1000/secureboot/create_keys_and_sign.sh \ ${WORKSPACE}/iot-platform-assets/corstone1000/secureboot/create_keys_and_sign.sh \
-d a320-fvp \ -d fvp \
-v ${CERTIFICATE_VALIDITY_DURATION_IN_DAYS} -v ${CERTIFICATE_VALIDITY_DURATION_IN_DAYS}
``` ```
@@ -979,7 +978,7 @@ To run an unsigned image boot test:
``` ```
corstone1000# \ corstone1000# \
load mmc 1:1 $loadaddr corstone1000_secureboot_a320-fvp_images/Image_a320-fvp; \ load mmc 1:1 $loadaddr corstone1000_secureboot_fvp_images/Image_fvp; \
loadm $loadaddr $kernel_addr_r $filesize; \ loadm $loadaddr $kernel_addr_r $filesize; \
bootefi $kernel_addr_r $fdtcontroladdr bootefi $kernel_addr_r $fdtcontroladdr
@@ -998,7 +997,7 @@ Load the signed kernel image.
``` ```
corstone1000# \ corstone1000# \
load mmc 1:1 $loadaddr corstone1000_secureboot_a320-fvp_images/Image_a320-fvp.signed; \ load mmc 1:1 $loadaddr corstone1000_secureboot_fvp_images/Image_fvp.signed; \
loadm $loadaddr $kernel_addr_r $filesize; \ loadm $loadaddr $kernel_addr_r $filesize; \
bootefi $kernel_addr_r $fdtcontroladdr bootefi $kernel_addr_r $fdtcontroladdr
``` ```
@@ -1037,7 +1036,7 @@ to communicate between the normal world and the secure world to run the [Arm Pla
The tests use the `arm_tstee` driver to access Trusted Services Secure Partitions from user space. The driver is included in the Linux Kernel, starting from v6.10. The tests use the `arm_tstee` driver to access Trusted Services Secure Partitions from user space. The driver is included in the Linux Kernel, starting from v6.10.
1. Start the Corstone-1000 with Cortex-A320 FVP and wait until it boots to Linux. 1. Start the Corstone-1000 FVP and wait until it boots to Linux.
2. Run the PSA API tests by running the commands below in the order shown: 2. Run the PSA API tests by running the commands below in the order shown:
@@ -1059,7 +1058,7 @@ Symmetric multiprocessing (SMP) mode is supported on the Corstone-1000 with Cort
meta-arm/kas/corstone1000-multicore.yml meta-arm/kas/corstone1000-multicore.yml
``` ```
2. Run the Corstone-1000 with Cortex-A320 FVP. 2. Run the Corstone-1000 FVP.
``` ```
kas shell meta-arm/kas/corstone1000-a320-fvp.yml:meta-arm/ci/debug.yml:\ kas shell meta-arm/kas/corstone1000-a320-fvp.yml:meta-arm/ci/debug.yml:\
@@ -1083,7 +1082,7 @@ To build on Ethos-U85 NPU:
``` ```
cd ${WORKSPACE} cd ${WORKSPACE}
git clone https://git.gitlab.arm.com/arm-reference-solutions/iot-platform-assets.git \ git clone https://git.gitlab.arm.com/arm-reference-solutions/iot-platform-assets.git \
-b CORSTONE1000-2026.11 -b CORSTONE1000-2026.05
``` ```
2. Copy the additional kas configuration file to: 2. Copy the additional kas configuration file to:
@@ -1108,14 +1107,14 @@ To build on Ethos-U85 NPU:
cd ${WORKSPACE} cd ${WORKSPACE}
``` ```
5. Re-build the Corstone-1000 with Cortex-A320 FVP software stack as follows: 5. Re-build the Corstone-1000 FVP software stack as follows:
``` ```
kas build meta-arm/kas/corstone1000-a320-fvp.yml:meta-arm/ci/debug.yml:\ kas build meta-arm/kas/corstone1000-a320-fvp.yml:meta-arm/ci/debug.yml:\
meta-arm/kas/ethos-u85-test.yml meta-arm/kas/ethos-u85-test.yml
``` ```
6. Run the Corstone-1000 with Cortex-A320 FVP: 6. Run the Corstone-1000 FVP:
``` ```
kas shell meta-arm/kas/corstone1000-a320-fvp.yml:meta-arm/ci/debug.yml:\ kas shell meta-arm/kas/corstone1000-a320-fvp.yml:meta-arm/ci/debug.yml:\
@@ -1123,7 +1122,7 @@ To build on Ethos-U85 NPU:
-c "../meta-arm/scripts/runfvp" -c "../meta-arm/scripts/runfvp"
``` ```
7. To verify you are running the Corstone-1000 with Cortex-A320 FVP, build and run the FVP and inspect the CPU model 7. To verify you are running the Corstone-1000 FVP (Cortex-A320), build and run the FVP and inspect the CPU model
reported in `/proc/cpuinfo` as shown below. Inside the FVP shell, confirm the core type: reported in `/proc/cpuinfo` as shown below. Inside the FVP shell, confirm the core type:
``` ```
@@ -2,7 +2,7 @@
The Arm Corstone-1000 with Cortex-A320 software stack uses the Yocto Project to build a tiny Linux distribution suitable for the Arm Corstone-1000 with Cortex-A320 platform (kernel and initramfs filesystem less than 6 MB on the flash). The Arm Corstone-1000 with Cortex-A320 software stack uses the Yocto Project to build a tiny Linux distribution suitable for the Arm Corstone-1000 with Cortex-A320 platform (kernel and initramfs filesystem less than 6 MB on the flash).
The Corstone-1000 with Cortex-A320 software stack can be run on [Arm Corstone-1000 with Cortex-A320 FVP (Fixed Virtual Platform)](https://developer.arm.com/downloads/-/arm-ecosystem-fvps) and is built on top of Yocto Project's [Blacksail release]($meta_arm_repository_release_branch). The Corstone-1000 with Cortex-A320 software stack can be run on [Arm Corstone-1000 with Cortex-A320 FVP (Fixed Virtual Platform)](https://developer.arm.com/downloads/-/arm-ecosystem-fvps) and is built on top of Yocto Project's [Wrynose release]($meta_arm_repository_release_branch).
The Yocto Project relies on the [BitBake](https://docs.yoctoproject.org/bitbake.html#bitbake-documentation) tool as its build tool. Please see the [Yocto Project documentation](https://docs.yoctoproject.org/) for more information. The Yocto Project relies on the [BitBake](https://docs.yoctoproject.org/bitbake.html#bitbake-documentation) tool as its build tool. Please see the [Yocto Project documentation](https://docs.yoctoproject.org/) for more information.
@@ -135,7 +135,7 @@ Table: Trusted Firmware-M secure enclave components
| Type | Path | | Type | Path |
| --------- | ---------------------------------------------------------------------------------------------------------------- | | --------- | ---------------------------------------------------------------------------------------------------------------- |
| bbappend | `${WORKSPACE}/meta-arm/meta-arm-bsp/recipes-bsp/trusted-firmware-m/trusted-firmware-m_%.bbappend` | | bbappend | `${WORKSPACE}/meta-arm/meta-arm-bsp/recipes-bsp/trusted-firmware-m/trusted-firmware-m_%.bbappend` |
| Recipe | `${WORKSPACE}/meta-arm/meta-arm/recipes-bsp/trusted-firmware-m/trusted-firmware-m_2.3.1.bb` | | Recipe | `${WORKSPACE}/meta-arm/meta-arm/recipes-bsp/trusted-firmware-m/trusted-firmware-m_2.3.0.bb` |
## Build {.reference} ## Build {.reference}
@@ -164,7 +164,7 @@ Building binaries natively on Windows and AArch64 Linux is not supported. Use an
``` ```
cd ${WORKSPACE} cd ${WORKSPACE}
git clone https://git.yoctoproject.org/git/meta-arm -b CORSTONE1000-2026.11 git clone https://git.yoctoproject.org/git/meta-arm -b CORSTONE1000-2026.05
``` ```
4. Accept the EULA on the [Arm Developer](https://developer.arm.com/downloads/-/arm-ecosystem-fvps/eula) site to build a Corstone-1000 with Cortex-A320 image for FVP as follows: 4. Accept the EULA on the [Arm Developer](https://developer.arm.com/downloads/-/arm-ecosystem-fvps/eula) site to build a Corstone-1000 with Cortex-A320 image for FVP as follows:
@@ -10,68 +10,6 @@ Change Log
This document contains a summary of the new features, changes and This document contains a summary of the new features, changes and
fixes in each release of Corstone-1000 software stack. fixes in each release of Corstone-1000 software stack.
***************
Version 2026.11
***************
Changes
=======
- Upgraded key Corstone-1000 components to U-Boot 2026.07, TF-A 2.15.0, TF-M 2.3.1, OP-TEE 4.10.0, and Linux 6.19.
- Updated TF-M to include Corstone-1000 security fixes and the latest firmware-update partition fixes.
Corstone-1000 components versions
=================================
+-------------------------------------------+-------------------+
| linux-yocto | 6.19 |
+-------------------------------------------+-------------------+
| u-boot | 2026.07 |
+-------------------------------------------+-------------------+
| external-system | 0.1.0 |
+-------------------------------------------+-------------------+
| optee-client | 4.10.0 |
+-------------------------------------------+-------------------+
| optee-os | 4.10.0 |
+-------------------------------------------+-------------------+
| trusted-firmware-a | 2.15.0 |
+-------------------------------------------+-------------------+
| trusted-firmware-m | 2.3.1 |
+-------------------------------------------+-------------------+
| libts | v1.3.0 |
+-------------------------------------------+-------------------+
| ts-sp-{se-proxy, smm-gateway} | v1.3.0 |
+-------------------------------------------+-------------------+
| ts-psa-{crypto, iat, its. ps}-api-test | 74dc6646ff |
+-------------------------------------------+-------------------+
Yocto distribution components versions
======================================
+-------------------------------------------+----------------+
| meta-arm | blacksail |
+-------------------------------------------+----------------+
| bitbake | 634f8dede6 |
+-------------------------------------------+----------------+
| meta-openembedded | a9d69bc066 |
+-------------------------------------------+----------------+
| openembedded-core | e182a0c4b9 |
+-------------------------------------------+----------------+
| meta-yocto | d0119aa2eb |
+-------------------------------------------+----------------+
| meta-secure-core | 56316bacdb |
+-------------------------------------------+----------------+
| busybox | 1.38.0 |
+-------------------------------------------+----------------+
| musl | 1.2.6 |
+-------------------------------------------+----------------+
| gcc-arm-none-eabi | 15.3.rel1 |
+-------------------------------------------+----------------+
| gcc-cross-aarch64 | 16.2.0 |
+-------------------------------------------+----------------+
| openssl | 4.0.3 |
+-------------------------------------------+----------------+
*************** ***************
Version 2026.05 Version 2026.05
*************** ***************
@@ -19,12 +19,6 @@ intended for safety-critical applications. Should Your Software or Your Hardware
prove defective, you assume the entire cost of all necessary servicing, repair prove defective, you assume the entire cost of all necessary servicing, repair
or correction. or correction.
***********************
Release notes - 2026.11
***********************
The same notes as the 2025.05 release still apply.
*********************** ***********************
Release notes - 2026.05 Release notes - 2026.05
*********************** ***********************
@@ -51,7 +51,7 @@ The Corstone-1000 software stack can be run on:
Yocto Stable Branch Yocto Stable Branch
------------------- -------------------
Corstone-1000 software stack is built on top of Yocto Project's `Blacksail release <meta-arm-repository-release-branch_>`__. Corstone-1000 software stack is built on top of Yocto Project's `Wrynose release <meta-arm-repository-release-branch_>`__.
Software Components Software Components
------------------- -------------------
@@ -173,7 +173,7 @@ Secure Enclave Components
+----------+-------------------------------------------------------------------------------------------------------+ +----------+-------------------------------------------------------------------------------------------------------+
| bbappend | ``${WORKSPACE}/meta-arm/meta-arm-bsp/recipes-bsp/trusted-firmware-m/trusted-firmware-m_%.bbappend`` | | bbappend | ``${WORKSPACE}/meta-arm/meta-arm-bsp/recipes-bsp/trusted-firmware-m/trusted-firmware-m_%.bbappend`` |
+----------+-------------------------------------------------------------------------------------------------------+ +----------+-------------------------------------------------------------------------------------------------------+
| Recipe | ``${WORKSPACE}/meta-arm/meta-arm/recipes-bsp/trusted-firmware-m/trusted-firmware-m_2.3.1.bb`` | | Recipe | ``${WORKSPACE}/meta-arm/meta-arm/recipes-bsp/trusted-firmware-m/trusted-firmware-m_2.3.0.bb`` |
+----------+-------------------------------------------------------------------------------------------------------+ +----------+-------------------------------------------------------------------------------------------------------+
************************************ ************************************
@@ -233,7 +233,7 @@ Build
.. code-block:: console .. code-block:: console
cd ${WORKSPACE} cd ${WORKSPACE}
git clone https://git.yoctoproject.org/git/meta-arm -b CORSTONE1000-2026.11 git clone https://git.yoctoproject.org/git/meta-arm -b CORSTONE1000-2026.05
#. Build a Corstone-1000 image: #. Build a Corstone-1000 image:
@@ -322,7 +322,7 @@ Flash
Using smaller cards or unsupported file systems (e.g., exFAT, NTFS) may cause the flashing Using smaller cards or unsupported file systems (e.g., exFAT, NTFS) may cause the flashing
process to fail or the device to become unresponsive. process to fail or the device to become unresponsive.
#. Download the FPGA bit file image ``AN550: Arm® Corstone™-1000 for MPS3 Version 2.0 r2p0-00rel0`` #. Download the FPGA bit file image ``AN550: Arm® Corstone™-1000 for MPS3 Version 2.0``
on the `Arm Developer website <https://developer.arm.com/tools-and-software/development-boards/fpga-prototyping-boards/download-fpga-images>`__. on the `Arm Developer website <https://developer.arm.com/tools-and-software/development-boards/fpga-prototyping-boards/download-fpga-images>`__.
Click on the ``Download AN550 bundle`` button and login to download the file. Click on the ``Download AN550 bundle`` button and login to download the file.
@@ -544,8 +544,8 @@ Tests
Reports Reports
------- -------
Reports for the tests conducted on the `Corstone-1000 software (CORSTONE1000-2026.11) <https://git.yoctoproject.org/meta-arm/tag/?h=CORSTONE1000-2026.11>`__ Reports for the tests conducted on the `Corstone-1000 software (CORSTONE1000-2026.05) <https://git.yoctoproject.org/meta-arm/tag/?h=CORSTONE1000-2026.05>`__
release are available for reference `here <https://gitlab.arm.com/arm-reference-solutions/arm-reference-solutions-test-report/-/tree/CORSTONE1000-2026.11/embedded-a/corstone1000/CORSTONE1000-2026.11?ref_type=tags>`__. release are available for reference `here <https://gitlab.arm.com/arm-reference-solutions/arm-reference-solutions-test-report/-/tree/CORSTONE1000-2026.05/embedded-a/corstone1000/CORSTONE1000-2026.05?ref_type=tags>`__.
.. _clean-secure-flash: .. _clean-secure-flash:
@@ -564,7 +564,7 @@ Clean Secure Flash
.. code-block:: console .. code-block:: console
cd ${WORKSPACE} cd ${WORKSPACE}
git clone https://git.gitlab.arm.com/arm-reference-solutions/iot-platform-assets.git -b CORSTONE1000-2026.11 git clone https://git.gitlab.arm.com/arm-reference-solutions/iot-platform-assets.git -b CORSTONE1000-2026.05
#. Copy the secure flash cleaning Git patch file to your copy of `meta-arm`. #. Copy the secure flash cleaning Git patch file to your copy of `meta-arm`.
@@ -691,7 +691,7 @@ FVP
=== ===
The ESP disk image will automatically be used by the Corstone-1000 FVP as the 2nd MMC card image. The ESP disk image will automatically be used by the Corstone-1000 FVP as the 2nd MMC card image.
It will be used when the SystemReady-IR tests are performed on the FVP in the later section. It will be used when the SystemReady-IR tests is performed on the FVP in the later section.
**************************** ****************************
@@ -749,7 +749,7 @@ The ``BOOT`` partition is also used to store test results in the ``acs_results``
Ensure that the ``acs_results`` folder is empty before starting the test. Ensure that the ``acs_results`` folder is empty before starting the test.
This section below describe how to build and run ACS tests on Corstone-1000. This sections below describe how to build and run ACS tests on Corstone-1000.
.. _mps3-instructions-for-acs-image: .. _mps3-instructions-for-acs-image:
@@ -1126,7 +1126,7 @@ Positive Full Capsule Update Test
The capsule update will be started. The capsule update will be started.
.. note:: .. note::
The capsule update takes about 8 minutes to complete on MPS3 and between 10-15 minutes on FVP. The capsule update takes about 8 minutes to complete on MPS3 and between 15-30 minutes on FVP.
The Corstone-1000 will reset after successfully applying the capsule. The Corstone-1000 will reset after successfully applying the capsule.
@@ -1437,13 +1437,13 @@ The following table shows the details of the first four ESRT entries for the pos
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| ``capsule_flags`` | ``fw_class`` | ``fw_type`` | ``fw_version`` | ``last_attempt_status`` | ``last_attempt_version`` | ``lowest_supported_fw_ver`` | | ``capsule_flags`` | ``fw_class`` | ``fw_type`` | ``fw_version`` | ``last_attempt_status`` | ``last_attempt_version`` | ``lowest_supported_fw_ver`` |
+===================+=======================+=============+================+=========================+==========================+=============================+ +===================+=======================+=============+================+=========================+==========================+=============================+
| 0 | ``${BL2_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${BL2_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${TFM_S_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${TFM_S_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${FIP_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${FIP_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${INITRAMFS_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${INITRAMFS_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
Positive Partial Capsule Update Test ESRT Positive Partial Capsule Update Test ESRT
@@ -1454,13 +1454,13 @@ The following table shows the details of the first four ESRT entries for the pos
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| ``capsule_flags`` | ``fw_class`` | ``fw_type`` | ``fw_version`` | ``last_attempt_status`` | ``last_attempt_version`` | ``lowest_supported_fw_ver`` | | ``capsule_flags`` | ``fw_class`` | ``fw_type`` | ``fw_version`` | ``last_attempt_status`` | ``last_attempt_version`` | ``lowest_supported_fw_ver`` |
+===================+=======================+=============+================+=========================+==========================+=============================+ +===================+=======================+=============+================+=========================+==========================+=============================+
| 0 | ``${BL2_GUID}`` | 1 | 7 | 0 | 7 | 0 | | 0 | ``${BL2_GUID}`` | 0 | 7 | 0 | 7 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${TFM_S_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${TFM_S_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${FIP_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${FIP_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${INITRAMFS_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${INITRAMFS_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+-----------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
Rollback Protection Capsule Update Test ESRT Rollback Protection Capsule Update Test ESRT
@@ -1471,13 +1471,13 @@ The following table shows the details of the first four ESRT entries for the rol
+-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| ``capsule_flags`` | ``fw_class`` | ``fw_type`` | ``fw_version`` | ``last_attempt_status`` | ``last_attempt_version`` | ``lowest_supported_fw_ver`` | | ``capsule_flags`` | ``fw_class`` | ``fw_type`` | ``fw_version`` | ``last_attempt_status`` | ``last_attempt_version`` | ``lowest_supported_fw_ver`` |
+===================+========================+=============+================+=========================+==========================+=============================+ +===================+========================+=============+================+=========================+==========================+=============================+
| 0 | ``${BL2_GUID}`` | 1 | 7 | 1 | 5 | 0 | | 0 | ``${BL2_GUID}`` | 0 | 7 | 1 | 5 | 0 |
+-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${TFM_S_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${TFM_S_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${FIP_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${FIP_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
| 0 | ``${INITRAMFS_GUID}`` | 1 | 6 | 0 | 6 | 0 | | 0 | ``${INITRAMFS_GUID}`` | 0 | 6 | 0 | 6 | 0 |
+-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+ +-------------------+------------------------+-------------+----------------+-------------------------+--------------------------+-----------------------------+
See the `UEFI documentation <https://uefi.org/specs/UEFI/2.10/23_Firmware_Update_and_Reporting.html#id29>`__ for more information on the significance of the table fields. See the `UEFI documentation <https://uefi.org/specs/UEFI/2.10/23_Firmware_Update_and_Reporting.html#id29>`__ for more information on the significance of the table fields.
@@ -1486,7 +1486,7 @@ See the `UEFI documentation <https://uefi.org/specs/UEFI/2.10/23_Firmware_Update
Linux Distributions Linux Distributions
------------------- -------------------
This section describes the steps to install major Linux distributions to the Corstone-1000 Host Processor. This sections describes the steps to install major Linux distributions to the Corstone-1000 Host Processor.
The Linux distributions to be installed are: The Linux distributions to be installed are:
@@ -1512,7 +1512,7 @@ Follow the instructions below to create the installation media.
The location of the ISO file on the development machine will be referred to as ``${DISTRO_INSTALLER_ISO_PATH}``. The location of the ISO file on the development machine will be referred to as ``${DISTRO_INSTALLER_ISO_PATH}``.
#. Create the installation media which will contain the necessary files to install the operating system. #. Create the installation media which will contain the necessary files to install the operation system.
- **MPS3**: - **MPS3**:
@@ -1553,7 +1553,7 @@ Corstone-1000 on-board non-volatile storage size is insufficient for installing
#. Do not yet connect this blank USB drive to the MPS3. It will be used as the primary drive to boot the distribution. #. Do not yet connect this blank USB drive to the MPS3. It will be used as the primary drive to boot the distribution.
- FVP: - FVP:
#. Create a 10 GB GUID Partition Table (GPT) formatted MultiMediaCard (MMC) image. #. Create an 10 GB GUID Partition Table (GPT) formatted MultiMediaCard (MMC) image.
.. code-block:: console .. code-block:: console
@@ -1674,13 +1674,6 @@ Boot Distribution
#. Once the installation is complete, unplug the installation media. #. Once the installation is complete, unplug the installation media.
#. Perform a cold boot of the MPS3. #. Perform a cold boot of the MPS3.
.. note::
As a workaround, if U-Boot does not detect the USB drive that contains
the installed operating system, copy the contents of that USB drive to
another supported USB drive that U-Boot can detect, then boot from the
new USB drive.
- FVP - FVP
The target should automatically boot into the installed operating system image. The target should automatically boot into the installed operating system image.
@@ -1795,7 +1788,7 @@ Generate Keys, Signed Image and Unsigned Image
cd ${WORKSPACE} cd ${WORKSPACE}
git clone https://gitlab.arm.com/arm-reference-solutions/iot-platform-assets \ git clone https://gitlab.arm.com/arm-reference-solutions/iot-platform-assets \
-b CORSTONE1000-2026.11 -b CORSTONE1000-2026.05
#. Set the current working directory to build directory's subdirectory containing the software stack build images. #. Set the current working directory to build directory's subdirectory containing the software stack build images.
@@ -2179,12 +2172,10 @@ For more information about this, see the following resources:
#. Enable the built-in PSA-ADAC Secure Debug Manager in the copied database. #. Enable the built-in PSA-ADAC Secure Debug Manager in the copied database.
In In
``${CS1000_ADAC_DB}/Boards/Arm_Development_Boards/MPS3_Corstone_1000_ADAC_611/project_types.xml``, ``${CS1000_ADAC_DB}/Boards/Arm_Development_Boards/MPS3_Corstone_1000_ADAC_611/project_types.xml``,
add the ``secure_debug_manager`` element immediately below the existing add the following element alongside the existing ``dtsl_config`` parameter:
``dtsl_config`` parameter:
.. code-block:: xml .. code-block:: xml
<param default="DtslScript" id="dtsl_config" type="string" visible="false"/>
<secure_debug_manager type="adac_sdc600" manifest="CDB://../../../ATP/manifest.xml" device="CSAPBCOM" reset_type="nSRST"/> <secure_debug_manager type="adac_sdc600" manifest="CDB://../../../ATP/manifest.xml" device="CSAPBCOM" reset_type="nSRST"/>
This selects the built-in PSA-ADAC implementation, its authentication-token This selects the built-in PSA-ADAC implementation, its authentication-token
@@ -2202,8 +2193,7 @@ For more information about this, see the following resources:
DPv3 is required so that Arm Development Studio uses the ADIv6 debug-port DPv3 is required so that Arm Development Studio uses the ADIv6 debug-port
addressing needed to reach ``CSAPBCOM``. addressing needed to reach ``CSAPBCOM``.
In the same file, rename the platform to a unique title in the In the same file, give the copied platform a unique title:
``<extension name="ds">`` extension block:
.. code-block:: xml .. code-block:: xml
@@ -2311,7 +2301,7 @@ For more information about this, see the following resources:
*Copyright (c) 2022-2026, Arm Limited. All rights reserved.* *Copyright (c) 2022-2026, Arm Limited. All rights reserved.*
.. _arm-developer-fvp: https://developer.arm.com/tools-and-software/open-source-software/arm-platforms-software/arm-ecosystem-fvps .. _arm-developer-fvp: https://developer.arm.com/tools-and-software/open-source-software/arm-platforms-software/arm-ecosystem-fvps
.. _meta-arm-repository-release-branch: https://docs.yoctoproject.org/next/migration-guides/migration-6.1.html .. _meta-arm-repository-release-branch: https://docs.yoctoproject.org/next/migration-guides/migration-6.0.html
.. _arm-ds-website: https://www.arm.com/products/development-tools/embedded-and-software/arm-development-studio .. _arm-ds-website: https://www.arm.com/products/development-tools/embedded-and-software/arm-development-studio
.. _arm-ulink-pro-d-website: https://www.arm.com/products/development-tools/debug-probes/ulink-pro .. _arm-ulink-pro-d-website: https://www.arm.com/products/development-tools/debug-probes/ulink-pro
.. _arm-dstream-website: https://www.arm.com/products/development-tools/debug-probes/dstream-st .. _arm-dstream-website: https://www.arm.com/products/development-tools/debug-probes/dstream-st
@@ -12,8 +12,8 @@ LIC_FILES_CHKSUM:aarch64 = "file://share/doc/gcc/Copying.html;md5=90014a59d1783b
LIC_FILES_CHKSUM:x86-64 = "file://share/doc/gcc/Copying.html;md5=90014a59d1783b37a10240d4d0002c6e" LIC_FILES_CHKSUM:x86-64 = "file://share/doc/gcc/Copying.html;md5=90014a59d1783b37a10240d4d0002c6e"
SRC_URI = "https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/${PV}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}.tar.xz;name=gcc-${HOST_ARCH}" SRC_URI = "https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/${PV}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}.tar.xz;name=gcc-${HOST_ARCH}"
SRC_URI[gcc-x86_64.sha256sum] = "1e4dc409cf4d33ef0a2e92c345f9e340302d355858de9bc3353758b6463895b0" SRC_URI[gcc-aarch64.sha256sum] = "a1c6fdda8b479ea3e235d38dc0994790b840648b60e9fbaf88c82ca117a7a2df"
SRC_URI[gcc-aarch64.sha256sum] = "63942f3f86e69d7beefa2b946bdb23437628529e7b29f97d32229f65d0905e40" SRC_URI[gcc-x86_64.sha256sum] = "1b07847728d455f18895f1ebd5d71a40f2ccb7cb3a84ca9a874d7f961a318ce4"
S = "${UNPACKDIR}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}" S = "${UNPACKDIR}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}"
@@ -12,8 +12,8 @@ LIC_FILES_CHKSUM:aarch64 = "file://share/doc/gcc/Copying.html;md5=90014a59d1783b
LIC_FILES_CHKSUM:x86-64 = "file://share/doc/gcc/Copying.html;md5=90014a59d1783b37a10240d4d0002c6e" LIC_FILES_CHKSUM:x86-64 = "file://share/doc/gcc/Copying.html;md5=90014a59d1783b37a10240d4d0002c6e"
SRC_URI = "https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/${PV}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}.tar.xz;name=gcc-${HOST_ARCH}" SRC_URI = "https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/${PV}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}.tar.xz;name=gcc-${HOST_ARCH}"
SRC_URI[gcc-aarch64.sha256sum] = "a0dc18893171d39dec82fd8d57eeab3da836fd3df4b61978db3eccbf4a6bb948" SRC_URI[gcc-aarch64.sha256sum] = "06979e0c8171de58e5dc2a2b2019330a290f30930f27728af98a83e1a7369b3a"
SRC_URI[gcc-x86_64.sha256sum] = "463df9665c246fa13bab2102115bbf83440661b6eb70b44f3dfd4712ca30037a" SRC_URI[gcc-x86_64.sha256sum] = "563bebb2b97d53382b956d6ee1fe61e2cae26699901417234a37df505ef9b5fa"
S = "${UNPACKDIR}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}" S = "${UNPACKDIR}/arm-gnu-toolchain-${PV}-${HOST_ARCH}-${BINNAME}"
@@ -13,7 +13,7 @@ SRC_URI = "gitsm://git.trustedfirmware.org/hafnium/hafnium.git;protocol=https;br
file://0001-arm-hafnium-fix-kernel-tool-linking.patch \ file://0001-arm-hafnium-fix-kernel-tool-linking.patch \
file://0001-work-around-visibility-issue.patch;patchdir=third_party/dtc \ file://0001-work-around-visibility-issue.patch;patchdir=third_party/dtc \
" "
SRCREV = "676ab03c6a3d78f10ddc0f8b96c02475da24392f" SRCREV = "ce12c6e53838f1cf07d50b616b72db57a81539a4"
B = "${WORKDIR}/build" B = "${WORKDIR}/build"
COMPATIBLE_MACHINE ?= "invalid" COMPATIBLE_MACHINE ?= "invalid"
@@ -12,10 +12,6 @@ LIC_FILES_CHKSUM = "file://docs/license.rst;md5=6ed7bace7b0bc63021c6eba7b524039e
SRCREV = "da738d5eae93af342fdc4995dd3c05acb4c9d757" SRCREV = "da738d5eae93af342fdc4995dd3c05acb4c9d757"
SRCBRANCH = "master" SRCBRANCH = "master"
# The tools use the TF-A source when performing a version check, not
# the version of this within the source. So, disable checking
UPSTREAM_VERSION_UNKNOWN = "1"
inherit python_poetry_core inherit python_poetry_core
BBCLASSEXTEND = "native nativesdk" BBCLASSEXTEND = "native nativesdk"
@@ -44,8 +44,3 @@ do_deploy() {
} }
addtask deploy after do_install addtask deploy after do_install
# FIXME - The RMM build embeds TMPDIR paths in the generated binaries.
# Skip the buildpaths QA check until these references can be removed.
INSANE_SKIP:${PN} += "buildpaths"
INSANE_SKIP:${PN}-dbg += "buildpaths"
@@ -1,4 +1,4 @@
From 0debd3a1f4b5abf0c75c26ec1e85d9fa33510c89 Mon Sep 17 00:00:00 2001 From 7d2f6b6e31fad2c96c6fc55c17e79f8c96b4bfcf Mon Sep 17 00:00:00 2001
From: Yeoreum Yun <yeoreum.yun@arm.com> From: Yeoreum Yun <yeoreum.yun@arm.com>
Date: Mon, 10 Aug 2026 15:40:06 +0100 Date: Mon, 10 Aug 2026 15:40:06 +0100
Subject: [PATCH] DynamicTablesPkg: fix SmbiosType2Genertor build failure with Subject: [PATCH] DynamicTablesPkg: fix SmbiosType2Genertor build failure with
@@ -36,3 +36,6 @@ index 1250ef8059e9..5345d2b6d233 100644
{ {
DEBUG ((DEBUG_ERROR, "%a: Invalid Baseboard object list\n", __func__)); DEBUG ((DEBUG_ERROR, "%a: Invalid Baseboard object list\n", __func__));
return EFI_INVALID_PARAMETER; return EFI_INVALID_PARAMETER;
--
2.39.5
@@ -1,127 +0,0 @@
From 1c89a7982247eb994956140caf794f4f40e85de9 Mon Sep 17 00:00:00 2001
From: santhoshkumarv <santhoshkumarv@ami.com>
Date: Mon, 28 Sep 2026 06:06:24 -0700
Subject: [PATCH] NetworkPkg/UefiPxeBcDxe: Fix ICMP buffer overflow in
IPv4/IPv6 handlers (CVE-2026-97631)
Root Cause:
Incorrect buffer copy logic in ICMP error handling allowed large packets
to write beyond the allocated buffer. This could overwrite adjacent memory
with attacker-controlled data, causing memory corruption.
Resolution:
Added proper boundary checks and corrected buffer offset calculations
in IPv4 and IPv6 ICMP error handlers. Over sized ICMP error packets are now
safely truncated when the buffer is full, preventing memory corruption.
Signed-off-by: santhoshkumarV <santhoshkumarv@ami.com>
Upstream-Status: Backport [560ed9b5b146ed622d7eb7c40254a9099bd65b84]
CVE: CVE-2026-97631
Signed-off-by: Jon Mason <jon.mason@arm.com>
---
NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c | 68 ++++++++++----------------
1 file changed, 26 insertions(+), 42 deletions(-)
diff --git a/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c b/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c
index 887ea3e7d7d8..9e97ba4904c7 100644
--- a/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c
+++ b/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c
@@ -252,6 +252,7 @@ PxeBcIcmpErrorDpcHandle (
UINTN Index;
UINT32 CopiedLen;
UINT8 *IcmpError;
+ UINT32 FragmentLen;
Private = (PXEBC_PRIVATE_DATA *)Context;
Mode = &Private->Mode;
@@ -317,28 +318,19 @@ PxeBcIcmpErrorDpcHandle (
}
//
- // Copy the right ICMP error message into mode data.
+ // Copy ICMP error fragments into Mode->IcmpError in order. The maximum
+ // buffer size is sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR); truncate any excess.
//
- CopiedLen = 0;
- IcmpError = (UINT8 *)&Mode->IcmpError;
-
- for (Index = 0; Index < RxData->FragmentCount; Index++) {
- CopiedLen += RxData->FragmentTable[Index].FragmentLength;
- if (CopiedLen <= sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR)) {
- CopyMem (
- IcmpError,
- RxData->FragmentTable[Index].FragmentBuffer,
- RxData->FragmentTable[Index].FragmentLength
- );
- } else {
- CopyMem (
- IcmpError,
- RxData->FragmentTable[Index].FragmentBuffer,
- CopiedLen - sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR)
- );
- }
-
- IcmpError += CopiedLen;
+ for (Index = 0, IcmpError = (UINT8 *)&Mode->IcmpError, CopiedLen = 0;
+ (Index < RxData->FragmentCount) &&
+ (CopiedLen < sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR));
+ Index++, IcmpError += FragmentLen, CopiedLen += FragmentLen)
+ {
+ FragmentLen = MIN (
+ RxData->FragmentTable[Index].FragmentLength,
+ (UINT32)(sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR) - CopiedLen)
+ );
+ CopyMem (IcmpError, RxData->FragmentTable[Index].FragmentBuffer, FragmentLen);
}
ON_RECYCLE:
@@ -387,6 +379,7 @@ PxeBcIcmp6ErrorDpcHandle (
UINT8 Type;
UINT32 CopiedLen;
UINT8 *Icmp6Error;
+ UINT32 FragmentLen;
Private = (PXEBC_PRIVATE_DATA *)Context;
Mode = &Private->Mode;
@@ -449,28 +442,19 @@ PxeBcIcmp6ErrorDpcHandle (
}
//
- // Copy the right ICMP6 error message into mode data.
+ // Copy ICMP error fragments into Mode->IcmpError in order. The maximum
+ // buffer size is sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR); truncate any excess.
//
- CopiedLen = 0;
- Icmp6Error = (UINT8 *)&Mode->IcmpError;
-
- for (Index = 0; Index < RxData->FragmentCount; Index++) {
- CopiedLen += RxData->FragmentTable[Index].FragmentLength;
- if (CopiedLen <= sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR)) {
- CopyMem (
- Icmp6Error,
- RxData->FragmentTable[Index].FragmentBuffer,
- RxData->FragmentTable[Index].FragmentLength
- );
- } else {
- CopyMem (
- Icmp6Error,
- RxData->FragmentTable[Index].FragmentBuffer,
- CopiedLen - sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR)
- );
- }
-
- Icmp6Error += CopiedLen;
+ for (Index = 0, Icmp6Error = (UINT8 *)&Mode->IcmpError, CopiedLen = 0;
+ (Index < RxData->FragmentCount) &&
+ (CopiedLen < sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR));
+ Index++, Icmp6Error += FragmentLen, CopiedLen += FragmentLen)
+ {
+ FragmentLen = MIN (
+ RxData->FragmentTable[Index].FragmentLength,
+ (UINT32)(sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR) - CopiedLen)
+ );
+ CopyMem (Icmp6Error, RxData->FragmentTable[Index].FragmentBuffer, FragmentLen);
}
ON_RECYCLE:
@@ -1,38 +0,0 @@
From bff45da697fecf64927fbc38eacfcf41f20b763b Mon Sep 17 00:00:00 2001
From: Michael D Kinney <michael.d.kinney@intel.com>
Date: Wed, 14 Oct 2026 12:34:35 -0700
Subject: [PATCH] NetworkPkg/UefiPxeBcDxe: Clear ICMP error buffer before
copying
Zero the ICMP error buffer before copying fragments so short packets do not
leave stale bytes from a previous error.
Signed-off-by: Michael D Kinney <michael.d.kinney@intel.com>
Upstream-Status: Backport [fa789071ff0b29f1e568bfe153ef7f3d76bd60e4]
CVE: CVE-2026-97631
Signed-off-by: Jon Mason <jon.mason@arm.com>
---
NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c b/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c
index 9e97ba4904c7..7795e4f187d2 100644
--- a/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c
+++ b/NetworkPkg/UefiPxeBcDxe/PxeBcSupport.c
@@ -321,6 +321,7 @@ PxeBcIcmpErrorDpcHandle (
// Copy ICMP error fragments into Mode->IcmpError in order. The maximum
// buffer size is sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR); truncate any excess.
//
+ ZeroMem (&Mode->IcmpError, sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR));
for (Index = 0, IcmpError = (UINT8 *)&Mode->IcmpError, CopiedLen = 0;
(Index < RxData->FragmentCount) &&
(CopiedLen < sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR));
@@ -445,6 +446,7 @@ PxeBcIcmp6ErrorDpcHandle (
// Copy ICMP error fragments into Mode->IcmpError in order. The maximum
// buffer size is sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR); truncate any excess.
//
+ ZeroMem (&Mode->IcmpError, sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR));
for (Index = 0, Icmp6Error = (UINT8 *)&Mode->IcmpError, CopiedLen = 0;
(Index < RxData->FragmentCount) &&
(CopiedLen < sizeof (EFI_PXE_BASE_CODE_ICMP_ERROR));
@@ -4,8 +4,4 @@ SRCREV_edk2 ?= "2970e5699ba6267f3384ffab20f96647578aebc8"
SRCREV_edk2-platforms ?= "c98cdf8de9bd84a3267623e64f4388e21de69512" SRCREV_edk2-platforms ?= "c98cdf8de9bd84a3267623e64f4388e21de69512"
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:" FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
SRC_URI += " \ SRC_URI += "file://0001-DynamicTablesPkg-fix-SmbiosType2Genertor-build-failu.patch"
file://0001-DynamicTablesPkg-fix-SmbiosType2Genertor-build-failu.patch \
file://0002-NetworkPkg-UefiPxeBcDxe-Fix-ICMP-buffer-overflow-in-.patch \
file://0003-NetworkPkg-UefiPxeBcDxe-Clear-ICMP-error-buffer-befo.patch \
"
@@ -33,8 +33,8 @@ SRC_URI = "\
# As per optee-ftpm TA documentation, we have to use this SHA of MS TPM reference # As per optee-ftpm TA documentation, we have to use this SHA of MS TPM reference
SRCREV_ms-tpm ?= "98b60a44aba79b15fcce1c0d1e46cf5918400f6a" SRCREV_ms-tpm ?= "98b60a44aba79b15fcce1c0d1e46cf5918400f6a"
# v4.11.0 # v4.10.0
SRCREV_optee-ta ?= "f09f4cdad21028289d4e8fc573893b75f7228dc7" SRCREV_optee-ta ?= "a09269b15de635e1816fe832e26adfbfb44c5455"
SRCREV_FORMAT = "ms-tpm_optee-ta" SRCREV_FORMAT = "ms-tpm_optee-ta"
@@ -1,4 +0,0 @@
require recipes-security/optee/optee-client.inc
# v4.11.0
SRCREV = "da81fb7b30781029bc9d4acfddc7c60b08deb1f3"
@@ -1,7 +1,7 @@
require recipes-security/optee/optee-client.inc require recipes-security/optee/optee-client.inc
# v4.11.0 # v4.10.0
SRCREV = "da81fb7b30781029bc9d4acfddc7c60b08deb1f3" SRCREV = "9f5e90918093c1d1cd264d8149081b64ab7ba672"
PV .= "+git" PV .= "+git"
UPSTREAM_CHECK_COMMITS = "1" UPSTREAM_CHECK_COMMITS = "1"
@@ -1,4 +0,0 @@
require recipes-security/optee/optee-examples.inc
# v4.11.0
SRCREV = "71e8f3f320eef46a9369700bf30b91d67427d535"
@@ -1,7 +1,7 @@
require recipes-security/optee/optee-examples.inc require recipes-security/optee/optee-examples.inc
# v4.11.0 # v4.10.0
SRCREV = "71e8f3f320eef46a9369700bf30b91d67427d535" SRCREV = "934c7edb74a26e90f68024cf441073528444177f"
PV .= "+git" PV .= "+git"
UPSTREAM_CHECK_COMMITS = "1" UPSTREAM_CHECK_COMMITS = "1"
@@ -1,32 +0,0 @@
require recipes-security/optee/optee-os_${PV}.bb
SUMMARY = "OP-TEE Trusted OS TA devkit"
DESCRIPTION = "OP-TEE TA devkit for build TAs"
HOMEPAGE = "https://www.op-tee.org/"
DEPENDS += "python3-pycryptodome-native"
DEPENDS:append:toolchain-clang = " lld-native"
do_install() {
#install TA devkit
install -d ${D}${includedir}/optee/export-user_ta/
for f in ${B}/export-ta_${OPTEE_ARCH}/* ; do
cp -aR $f ${D}${includedir}/optee/export-user_ta/
done
}
do_deploy() {
echo "Do not inherit do_deploy from optee-os."
}
SPDX_DEPLOY_ALLOW_EMPTY:task-deploy = "1"
FILES:${PN} = "${includedir}/optee/"
# Build paths are currently embedded
INSANE_SKIP:${PN}-dev += "buildpaths"
# Include extra headers needed by SPMC tests to TA DEVKIT.
# Supported after op-tee v3.20
EXTRA_OEMAKE:append = "${@bb.utils.contains('MACHINE_FEATURES', 'optee-spmc-test', \
' CFG_SPMC_TESTS=y', '' , d)}"
@@ -1,6 +0,0 @@
require recipes-security/optee/optee-os.inc
DEPENDS += "dtc-native"
# v4.11.0
SRCREV = "f54190f22611dd5ad5d22546fd4209aa100f56bd"
@@ -2,8 +2,8 @@ require recipes-security/optee/optee-os.inc
DEPENDS += "dtc-native" DEPENDS += "dtc-native"
# v4.11.0 # v4.10.0
SRCREV = "f54190f22611dd5ad5d22546fd4209aa100f56bd" SRCREV = "753afbbee1682f5d16fd30e87b31058a4fd4f4b8"
PV .= "+git" PV .= "+git"
UPSTREAM_CHECK_COMMITS = "1" UPSTREAM_CHECK_COMMITS = "1"
@@ -1,17 +0,0 @@
require recipes-security/optee/optee-test.inc
# v4.11.0
SRCREV = "179740b43809069ee567d58ebd8e0f85ef8168e2"
LIC_FILES_CHKSUM = "file://LICENSE.md;md5=a8fa504109e4cd7ea575bc49ea4be560 \
file://LICENSE-BSD;md5=dca16d6efa93b55d0fd662ae5cd6feeb \
file://LICENSE-GPL;md5=10e86b5d2a6cb0e2b9dcfdd26a9ac58d \
"
# Include ffa_spmc test group if the SPMC test is enabled.
# Supported after op-tee v3.20
EXTRA_OEMAKE:append = "${@bb.utils.contains('MACHINE_FEATURES', 'optee-spmc-test', \
' CFG_SPMC_TESTS=y CFG_SECURE_PARTITION=y', '' , d)}"
RDEPENDS:${PN} += "${@bb.utils.contains('MACHINE_FEATURES', 'optee-spmc-test', \
' arm-ffa-user', '' , d)}"
@@ -1,7 +1,7 @@
require recipes-security/optee/optee-test.inc require recipes-security/optee/optee-test.inc
# v4.11.0 # v4.10.0
SRCREV = "179740b43809069ee567d58ebd8e0f85ef8168e2" SRCREV = "88c93e87a5c172363ee986ded036a25cafcc9d2c"
PV .= "+git" PV .= "+git"
UPSTREAM_CHECK_COMMITS = "1" UPSTREAM_CHECK_COMMITS = "1"
-279
View File
@@ -1,279 +0,0 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Arm Limited
"""Report dangling patches, unreferenced BSP includes, and candidate obsolete BSP recipes.
Static analysis only; no BitBake environment required. Recipe/include results are
review-only. Patch deletion is opt-in and still requires care with dynamic SRC_URI.
"""
import argparse
import fnmatch
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
META_EXT = {'.bb', '.bbappend', '.inc', '.conf', '.bbclass'}
ASSIGN = re.compile(r'^\s*([A-Za-z0-9_${}/.+%-]+(?::[A-Za-z0-9_${}/.+%-]+)*)\s*(?:\?\?=|\?=|:=|\+=|=\+|\.=|=\.|=)\s*(.*)$')
RECIPE = re.compile(r'^(.+)_([^_]+)\.(bb|bbappend)$')
TOKEN = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.+%-]*')
def git(cmd, root):
return subprocess.run(['git', '-C', str(root), *cmd], capture_output=True)
def load(root):
r = git(['rev-parse', '--show-toplevel'], root)
if r.returncode:
raise RuntimeError('Not in a Git checkout; specify --root')
root = Path(r.stdout.decode().strip()).resolve()
r = git(['ls-files', '-z'], root)
if r.returncode:
raise RuntimeError('git ls-files failed')
paths = [Path(p.decode('utf-8', 'surrogateescape')) for p in r.stdout.split(b'\0') if p]
sources = {}
for path in paths:
if path.suffix in META_EXT:
try:
sources[path] = (root / path).read_text(encoding='utf-8', errors='replace')
except OSError:
pass
return root, paths, sources
def in_bsp(path):
return path.parts and path.parts[0] == 'meta-arm-bsp'
def recipe_tree(path):
return any(part.startswith('recipes-') for part in path.parts)
def refs(name, sources, skip=None):
return [p for p, text in sources.items() if p != skip and name in text]
def patches(paths, sources):
for path in paths:
if path.suffix != '.patch' or not recipe_tree(path):
continue
found = refs(path.name, sources)
if not found:
yield ('patches', path, 'No literal filename reference in tracked metadata', True)
def includes(paths, sources):
for path in paths:
if not in_bsp(path) or path.suffix != '.inc':
continue
found = refs(path.name, sources, path)
if found:
continue
# A basename may be constructed from variable expansions, e.g.
# require ${BPN}.inc. Treat these as ambiguous rather than safe.
parent = path.parent
dynamic = [p for p, content in sources.items()
if p != path and (p.parent == parent or parent in p.parents)
and re.search(r'(?m)^\s*(?:require|include)\s+[^\n#]*\$\{', content)]
note = 'No literal include reference found'
if dynamic:
note += '; dynamic include(s) in ' + ', '.join(str(p) for p in dynamic[:3])
yield ('includes', path, note + ' (review only)', False)
def logical_lines(text):
return re.sub(r'\\\s*\n\s*', ' ', text).splitlines()
def metadata(sources):
selections = defaultdict(list)
mentions = defaultdict(set)
for path, content in sources.items():
for line in logical_lines(content):
line = line.split('#', 1)[0]
m = ASSIGN.match(line)
if not m:
continue
key, value = m.groups()
if key.startswith('PREFERRED_VERSION_'):
name = key[len('PREFERRED_VERSION_'):].split(':', 1)[0]
versions = re.findall(r'["\']([^"\']+)["\']', value)
if versions:
selections[name].extend((v.strip(), path) for v in versions)
continue
# Only dependency/provider/image variables imply usage. Merely
# mentioning a recipe in an arbitrary string is not sufficient.
base = key.split(':', 1)[0]
if (base in {'IMAGE_INSTALL', 'CORE_IMAGE_EXTRA_INSTALL', 'PACKAGE_INSTALL',
'MACHINE_ESSENTIAL_EXTRA_RDEPENDS', 'MACHINE_ESSENTIAL_EXTRA_RRECOMMENDS',
'MACHINE_EXTRA_RDEPENDS', 'MACHINE_EXTRA_RRECOMMENDS',
'DEPENDS', 'RDEPENDS', 'RRECOMMENDS', 'EXTRA_IMAGEDEPENDS',
'PREFERRED_PROVIDER_virtual/kernel', 'PREFERRED_PROVIDER_virtual/bootloader'}
or base.startswith(('RDEPENDS_', 'RRECOMMENDS_', 'PREFERRED_PROVIDER_'))):
for token in TOKEN.findall(value):
mentions[token].add(path)
return selections, mentions
def version_matches(version, pattern):
if '${' in pattern or '$(' in pattern:
return None
return fnmatch.fnmatchcase(version, pattern.replace('%', '*'))
def recipes(paths, sources, verbose=False):
selections, mentions = metadata(sources)
groups = defaultdict(list)
for path in paths:
if not in_bsp(path) or not recipe_tree(path):
continue
m = RECIPE.match(path.name)
if m:
name, version, kind = m.groups()
groups[name].append((path, version, kind))
for name, entries in sorted(groups.items()):
picks = selections.get(name, [])
usage = mentions.get(name, set())
# Version-specific recipes are candidates only when there is evidence
# of a version transition, or a vanished explicit pin. No pin and no
# references is suspicious, but not proof: default selection is valid.
for path, version, kind in entries:
if version == '%' or '${' in version:
continue
matched = [(p, src) for p, src in picks if version_matches(version, p)]
if matched:
if verbose:
yield ('referenced', path, 'Version selected by ' + ', '.join(str(src) for _, src in matched[:4]), False)
continue
unknown = [(p, src) for p, src in picks if version_matches(version, p) is None]
if unknown:
if verbose:
yield ('referenced', path, 'Dynamic PREFERRED_VERSION: cannot resolve statically', False)
continue
# Check versions of the same type, not unrelated bb vs bbappend.
peers = [(other, v) for other, v, k in entries if k == kind and other != path]
newer_or_selected = [v for _, v in peers if v != '%' and (not picks or any(version_matches(v, p) for p, _ in picks))]
if picks:
reason = 'No matching PREFERRED_VERSION; selected: ' + ', '.join(sorted({p for p, _ in picks}))
if not newer_or_selected:
reason += '; no alternative matching local version'
else:
reason = 'No PREFERRED_VERSION found'
if peers:
reason += '; other local versions: ' + ', '.join(sorted({v for _, v in peers}))
if usage:
reason += '; recipe/provider referenced in ' + ', '.join(str(p) for p in sorted(usage)[:3])
category = 'potentially obsolete'
elif picks or peers:
category = 'potentially obsolete'
reason += '; no direct image/dependency/provider reference found'
else:
category = 'likely unused'
reason += '; no direct image/dependency/provider reference found (default BitBake selection still possible)'
yield ('recipes', path, f'{category}: {reason}', False)
def main():
ap = argparse.ArgumentParser(
description='Find cleanup candidates in a meta-arm Git checkout (no BitBake required).',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Checks and scope:
patches All layers: tracked .patch files with no literal filename reference
in tracked BitBake metadata. Dynamic SRC_URI can cause false positives.
includes meta-arm-bsp only: .inc files without literal references; dynamic
include/require statements are noted where detected.
recipes meta-arm-bsp only: versioned .bb/.bbappend files compared with
PREFERRED_VERSION settings and image/dependency/provider mentions.
Results are grouped by confidence: likely unused, then potentially
obsolete. A recipe may be selected implicitly without a version pin.
all Run all checks (default).
Examples:
scripts/find-unused.py # Report everything
scripts/find-unused.py recipes --verbose # Explain BSP recipe findings
scripts/find-unused.py patches # Find dangling patches
scripts/find-unused.py includes # Review BSP include files
scripts/find-unused.py patches --delete # Confirm each patch removal
scripts/find-unused.py patches --delete --force
scripts/find-unused.py --root /path/to/meta-arm all
Safety:
Reporting is the default. Only dangling patches can be deleted; recipe
and include findings are ALWAYS review-only, even with --delete --force.
Searches use tracked files from Git but read the current working tree, so
uncommitted edits to machine configurations affect the results. No BitBake
parsing, overrides, providers, or dynamic variable expansions are resolved.
""")
ap.add_argument('check', nargs='?', choices=['all', 'patches', 'includes', 'recipes'], default='all', help='Check to run (default: all)')
ap.add_argument('--root', type=Path, default=Path.cwd(), help='Path inside the meta-arm Git checkout (default: current directory)')
ap.add_argument('--verbose', action='store_true', help='Show reasons and selected-version matches, including referenced recipes')
ap.add_argument('--delete', action='store_true', help='Prompt before deleting dangling patches only')
ap.add_argument('--force', action='store_true', help='With --delete, skip prompts')
ap.add_argument('--dry-run', action='store_true', help='Explicit report-only mode (the default)')
args = ap.parse_args()
if args.force and not args.delete:
ap.error('--force requires --delete')
if args.delete and args.dry_run:
ap.error('--delete conflicts with --dry-run')
try:
root, paths, sources = load(args.root)
except RuntimeError as exc:
ap.error(str(exc))
if args.verbose:
print(f'Repository: {root} | tracked files: {len(paths)} | metadata: {len(sources)}')
checks = {'patches': patches, 'includes': includes, 'recipes': recipes}
total = 0
for check, func in checks.items():
if args.check not in ('all', check):
continue
found = list(func(paths, sources, args.verbose) if check == 'recipes' else func(paths, sources))
visible = [f for f in found if f[0] != 'referenced']
total += len(visible)
print(f'\n{check.capitalize()} ({len(visible)} candidates):')
if check == 'recipes':
def recipe_rank(item):
category, path, reason, _ = item
if category == 'referenced':
return (2, str(path))
if reason.startswith('likely unused:'):
return (0, str(path))
return (1, str(path))
found.sort(key=recipe_rank)
last_level = None
for category, path, reason, safe in found:
if category == 'referenced' and not args.verbose:
continue
if check == 'recipes':
level = ('Referenced' if category == 'referenced' else
'Likely unused' if reason.startswith('likely unused:') else
'Potentially obsolete')
if level != last_level:
print(f' {level}:')
last_level = level
print(f' {path}')
else:
print(f' {path}')
if args.verbose or check == 'recipes':
print(f' {reason}' if check == 'recipes' else f' {reason}')
if args.delete and safe:
try:
yes = args.force or input(' Delete patch? [y/N] ').strip().lower() == 'y'
except EOFError:
yes = False
if yes:
try:
(root / path).unlink()
print(' Deleted')
except OSError as exc:
print(f' ERROR: {exc}', file=sys.stderr)
elif args.delete and not safe and category != 'referenced':
print(' Review only; not deleted')
print(f'\nTotal: {total} candidates. Static analysis; verify before removing files.')
return 0
if __name__ == '__main__':
sys.exit(main())
-731
View File
@@ -1,731 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Arm Limited
# SPDX-License-Identifier: MIT
"""Create, publish and announce meta-arm release tags."""
import argparse
from datetime import date, datetime
import os
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from email.message import EmailMessage
from pathlib import Path
SUPPORT_URL = 'https://docs.yoctoproject.org/ref-manual/release-process.html'
VERSION_RE = re.compile(r'^(\d+)\.(\d+)(?:\.(\d+))?$')
PLATFORM_TAG_RE = re.compile(r'^[A-Z][A-Z0-9_-]*-[0-9]{4}\.[0-9]{2}\.[0-9]{2}$')
DEFAULT_LTS = ('scarthgap', 'wrynose')
class ReleaseError(Exception):
pass
def run(*argv, check=True):
result = subprocess.run(argv, text=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if check and result.returncode:
raise ReleaseError(f"{' '.join(argv)}: {result.stderr.strip()}")
return result
def git(*args, check=True):
return run('git', *args, check=check)
def series_number(version):
match = VERSION_RE.fullmatch(version)
if not match or match.group(3) is not None or int(match.group(2)) > 3:
raise ReleaseError(f'Invalid initial release version: {version}')
return int(match.group(1)) * 4 + int(match.group(2))
def series_from_number(number):
return f'{number // 4}.{number % 4}'
def previous_series(version):
number = series_number(version)
if number == 0:
raise ReleaseError('No previous release version')
return series_from_number(number - 1)
def next_patch(version):
match = VERSION_RE.fullmatch(version)
if not match:
raise ReleaseError(f'Invalid version: {version}')
return f'{match.group(1)}.{match.group(2)}.{int(match.group(3) or 0) + 1}'
def platform_tag(name, release_date=None):
"""Build a platform tag, validating both platform name and calendar date."""
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]*', name):
raise ReleaseError(f'Invalid platform name: {name}')
if release_date is None:
release_date = date.today().strftime('%Y.%m.%d')
try:
datetime.strptime(release_date, '%Y.%m.%d')
except ValueError as exc:
raise ReleaseError('--date must be a valid YYYY.MM.DD date') from exc
if not re.fullmatch(r'\d{4}\.\d{2}\.\d{2}', release_date):
raise ReleaseError('--date must use YYYY.MM.DD')
return f'{name.upper()}-{release_date}'
@dataclass
class BranchStatus:
branch: str
series: str
last_tag: str | None
commits: int | None
next_tag: str
class ReleaseManager:
def __init__(self, remote='yoctoproject', recipient='meta-arm@lists.yoctoproject.org',
lts=DEFAULT_LTS, execute=False, edit=False, as_of=None, force_lts=False, mailer='auto'):
self.remote = remote
self.recipient = recipient
self.lts = tuple(lts)
self.execute = execute
self.edit = edit
self.as_of = as_of
self.force_lts = force_lts
if mailer not in ('auto', 'sendmail', 'mutt'):
raise ReleaseError(f'Invalid mailer: {mailer}')
self.mailer = mailer
self._branches = None
self._series = None
def branch_ref(self, branch):
return f'refs/remotes/{self.remote}/{branch}'
def branches(self):
if self._branches is None:
refs = git('for-each-ref', '--format=%(refname:strip=3)',
f'refs/remotes/{self.remote}/').stdout.splitlines()
self._branches = sorted(b for b in refs if b not in ('master', 'testing') and '/' not in b)
if not self._branches:
raise ReleaseError(f'No release branches under {self.remote}')
return self._branches
def require_branch(self, branch):
if branch not in self.branches():
raise ReleaseError(f'Unknown or excluded upstream branch: {branch}')
@staticmethod
def ancestor(older, newer):
return git('merge-base', '--is-ancestor', older, newer, check=False).returncode == 0
@staticmethod
def tag_commit(tag):
result = git('rev-parse', '--verify', '-q', f'refs/tags/{tag}^{{commit}}', check=False)
return result.stdout.strip() if result.returncode == 0 else None
@staticmethod
def tag_object(tag):
result = git('rev-parse', '--verify', '-q', f'refs/tags/{tag}', check=False)
return result.stdout.strip() if result.returncode == 0 else None
def numeric_tags(self):
return [tag for tag in git('tag', '-l').stdout.splitlines() if VERSION_RE.fullmatch(tag)]
def discover_series(self):
"""Use tags that lie on a branch, not inherited tags on its fork history.
A branch's initial release can be at the fork itself; in that case,
resolve shared tags by proximity and fail closed if ambiguous.
"""
if self._series is not None:
return self._series
branches = self.branches()
tags = self.numeric_tags()
forks = {}
for branch in branches:
result = git('merge-base', self.branch_ref(branch), self.branch_ref('master'))
forks[branch] = result.stdout.strip()
# For every series, assign it to the branch whose tip is nearest
# to its latest reachable tag. Ties cannot establish ownership.
candidates = {}
for tag in tags:
match = VERSION_RE.fullmatch(tag)
series = f'{match.group(1)}.{match.group(2)}'
for branch in branches:
if not self.ancestor(f'refs/tags/{tag}', self.branch_ref(branch)):
continue
distance = int(git('rev-list', '--count',
f'{tag}..{self.branch_ref(branch)}').stdout)
current = candidates.setdefault(series, {})
if branch not in current or distance < current[branch][0] or (
distance == current[branch][0] and
(int(match.group(3) or 0) > current[branch][1])):
current[branch] = (distance, int(match.group(3) or 0), tag)
owners = {}
for series, choices in candidates.items():
ranked = sorted((v[0], -v[1], b) for b, v in choices.items())
if not ranked:
continue
# If multiple branches are tied, avoid guessing ownership.
if len(ranked) > 1 and ranked[0][:2] == ranked[1][:2]:
raise ReleaseError(f'Ambiguous ownership for {series}: {ranked[0][2]}, {ranked[1][2]}')
owners[series] = ranked[0][2]
by_branch = {}
for series, branch in owners.items():
if branch in by_branch:
raise ReleaseError(f'Multiple version series appear to belong to {branch}')
by_branch[branch] = series
if not by_branch:
raise ReleaseError('No release version anchors found')
# Infer only unreleased branches, after the highest established series.
missing = [b for b in branches if b not in by_branch]
if missing:
latest = max(series_number(s) for s in by_branch.values())
ordered = sorted(missing, key=lambda b: int(git('show', '-s', '--format=%ct', forks[b]).stdout))
for earlier, later in zip(ordered, ordered[1:]):
if not self.ancestor(forks[earlier], forks[later]) or forks[earlier] == forks[later]:
raise ReleaseError(f'Cannot order unreleased branches: {earlier}, {later}')
for branch in ordered:
latest += 1
by_branch[branch] = series_from_number(latest)
self._series = by_branch
return by_branch
def latest_tag(self, branch):
series = self.discover_series()[branch]
choices = []
for tag in self.numeric_tags():
match = VERSION_RE.fullmatch(tag)
if f'{match.group(1)}.{match.group(2)}' != series:
continue
if self.ancestor(f'refs/tags/{tag}', self.branch_ref(branch)):
choices.append((int(match.group(3) or 0), tag))
return max(choices)[1] if choices else None
def status(self, branch):
self.require_branch(branch)
series = self.discover_series()[branch]
last = self.latest_tag(branch)
if not last:
return BranchStatus(branch, series, None, None, f'Initial {series}')
count = int(git('rev-list', '--count', f'{last}..{self.branch_ref(branch)}').stdout)
return BranchStatus(branch, series, last, count, next_patch(last) if count else '-')
def show_status(self, branches):
print(f'{"Branch":<18} {"Version":<9} {"Last-tag":<14} {"Commits":<10} Next-tag')
for branch in branches:
state = self.status(branch)
print(f'{branch:<18} {state.series:<9} {state.last_tag or "(none)":<14} '
f'{str(state.commits) if state.commits is not None else "-":<10} {state.next_tag}')
def create_tags(self, branch, version, ref, kind):
self.require_branch(branch)
if not VERSION_RE.fullmatch(version):
raise ReleaseError(f'Invalid tag version: {version}')
target = git('rev-parse', '--verify', f'{ref}^{{commit}}').stdout.strip()
if not self.ancestor(target, self.branch_ref(branch)):
raise ReleaseError(f'{ref} is not on {branch}')
for tag in (version, f'yocto-{version}'):
old = self.tag_commit(tag)
if old and old != target:
raise ReleaseError(f'{tag} already points to another commit')
print(f'{branch}: {version} -> {target[:12]} ({kind})')
for tag in (version, f'yocto-{version}'):
if self.tag_object(tag):
print(f' Exists: {tag}')
elif self.execute:
git('tag', '-s', tag, '-m', f'{kind} for Yocto Project {version} ({branch})', target)
print(f' Created: {tag}')
else:
print(f' DRY RUN: create {tag}')
self._series = None
def release(self, branch, version):
self.require_branch(branch)
expected = self.discover_series()[branch]
if version != expected:
raise ReleaseError(f'Expected {expected} for {branch}, got {version}')
ref = self.branch_ref(branch)
if self.as_of:
ref = git('rev-list', '--first-parent', '--before', f'{self.as_of} 23:59:59',
'-1', ref).stdout.strip()
if not ref:
raise ReleaseError(f'No commit on {branch} before {self.as_of}')
self.create_tags(branch, version, ref, 'Initial release')
def update(self, branches):
created = []
for branch in branches:
state = self.status(branch)
if not state.last_tag:
print(f'{branch}: initial release {state.series} missing; skipping')
elif not state.commits:
print(f'{branch}: no new commits; skipping')
else:
self.create_tags(branch, state.next_tag, self.branch_ref(branch), 'Update')
created.append((branch, state.next_tag))
return created
def platform(self, name, ref, release_date=None):
"""Create a signed platform tag without requiring release branch discovery."""
tag = platform_tag(name, release_date)
target = git('rev-parse', '--verify', f'{ref}^{{commit}}').stdout.strip()
existing = self.tag_object(tag)
if existing:
if self.tag_commit(tag) != target:
raise ReleaseError(f'{tag} already exists at another commit')
print(f'Platform tag already exists: {tag} -> {target[:12]}')
return tag
print(f'Platform: {name}\nCommit: {target[:12]}\nTag: {tag}')
if self.execute:
message = f'{name.upper()} platform release ({tag.rsplit("-", 1)[1]})\n\nRelease of the {name} platform at commit {target}.'
git('tag', '-s', tag, '-m', message, target)
print(f'Created signed tag: {tag}')
else:
print(f'DRY RUN: Would create signed tag {tag}.')
return tag
def tags_for(self, branches):
series_map = self.discover_series()
selected = branches or self.branches()
result = set()
for branch in selected:
self.require_branch(branch)
series = series_map[branch]
for tag in self.numeric_tags():
match = VERSION_RE.fullmatch(tag)
if f'{match.group(1)}.{match.group(2)}' != series:
continue
if not self.ancestor(f'refs/tags/{tag}', self.branch_ref(branch)):
continue
paired = f'yocto-{tag}'
if not self.tag_commit(paired) or self.tag_commit(paired) != self.tag_commit(tag):
raise ReleaseError(f'Missing or mismatched paired tag: {paired}')
result.update((tag, paired))
return sorted(result)
def push(self, selections=(), all_tags=False):
"""Push selected tags, or all recognized release tags, without overwrites.
For compatibility, a selection may be an existing release branch.
Explicit tag names need not match the recognized tag patterns.
"""
if all_tags:
tags = git('tag', '-l').stdout.splitlines()
elif selections:
tags = set()
for selection in selections:
if self.tag_object(selection):
tags.add(selection)
elif selection in self.branches():
tags.update(self.tags_for((selection,)))
else:
raise ReleaseError(f'Unknown tag or release branch: {selection}')
tags = sorted(tags)
else:
tags = [t for t in git('tag', '-l').stdout.splitlines()
if VERSION_RE.fullmatch(t)
or (t.startswith('yocto-') and VERSION_RE.fullmatch(t[6:]))
or PLATFORM_TAG_RE.fullmatch(t)]
if not tags:
print('No matching release tags')
return
remote = {}
for line in git('ls-remote', '--tags', '--refs', self.remote).stdout.splitlines():
sha, ref = line.split()
remote[ref.removeprefix('refs/tags/')] = sha
pending = []
published = 0
for tag in tags:
local = self.tag_object(tag)
old = remote.get(tag)
if old is None:
pending.append(tag)
elif old == local:
published += 1
else:
raise ReleaseError(f'Tag conflict on {self.remote}: {tag} local={local} remote={old}')
print(f'Upstream: {self.remote}\nAlready published: {published}\nNew tags: {len(pending)}')
for tag in pending:
print(f' {tag}')
if pending and self.execute:
git('push', '--atomic', self.remote,
*(f'refs/tags/{tag}:refs/tags/{tag}' for tag in pending))
print(f'Published {len(pending)} tags.')
elif not self.execute:
print('DRY RUN: No tags pushed.')
def lts_description(self, branch):
if branch in self.lts or self.force_lts:
opening = ('This is a Long Term Support (LTS) release. meta-arm generally aims '
'to follow the Yocto Project\'s LTS support lifecycle on a best-effort '
'basis, with maintenance primarily focused on bug fixes and security '
'fixes. CI testing is performed on supported configurations as resources permit.')
else:
opening = ('This is NOT a Long Term Support (LTS) release. meta-arm generally aims '
'to follow the Yocto Project\'s release support lifecycle on a best-effort '
'basis. For non-LTS releases, this means approximately seven months of '
'limited maintenance, primarily consisting of bug fixes and security fixes, '
'with minimal CI testing.')
if self.lts:
names = ', '.join(self.lts[:-1]) + (' and ' if len(self.lts) > 1 else '') + self.lts[-1]
opening += f'\n\nThe currently supported LTS branches are {names}.'
return opening + ('\n\nFor more information about the Yocto Project\'s release '
f'lifecycle and LTS policy, see:\n{SUPPORT_URL}')
def announcement(self, branch, version):
self.require_branch(branch)
series = self.discover_series()[branch]
if version != series and not re.fullmatch(re.escape(series) + r'\.\d+', version):
raise ReleaseError(f'{version} does not belong to {branch} ({series})')
if not self.tag_commit(version) or self.tag_commit(version) != self.tag_commit(f'yocto-{version}'):
raise ReleaseError(f'Missing or mismatched release tag pair: {version}')
if not self.ancestor(f'refs/tags/{version}', self.branch_ref(branch)):
raise ReleaseError(f'{version} is not on {branch}')
lts_text = self.lts_description(branch)
if version == series:
previous = previous_series(version)
if not self.tag_commit(previous):
raise ReleaseError(f'Missing previous initial release tag {previous}')
count = git('rev-list', '--count', f'{previous}..{version}').stdout.strip()
subject = f'meta-arm {branch} release'
body = (f'meta-arm has released the {branch} support branch, tagged as {version}.\n\n'
f'{lts_text}\n\nThis release contains {count} commits since the previous '
f'Yocto Project release, {previous}.')
else:
patch = int(version.rsplit('.', 1)[1])
previous = series if patch == 1 else f'{series}.{patch - 1}'
if not self.tag_commit(previous) or not self.ancestor(previous, version):
raise ReleaseError(f'Missing or non-ancestor previous tag: {previous}')
log = git('log', '--reverse', '--format=%h %s', f'{previous}..{version}').stdout.strip()
count = int(git('rev-list', '--count', f'{previous}..{version}').stdout)
if not count:
raise ReleaseError('No commits in update release')
dates = git('log', '--reverse', '--format=%cs', f'{previous}..{version}').stdout.splitlines()
first, last = dates[0], git('log', '-1', '--format=%cs', version).stdout.strip()
subject = f'meta-arm {branch} update'
body = (f'meta-arm has released an updated {branch} support tag, {version}.\n\n'
f'{lts_text}\n\nThis release contains {count} commits between {first} and {last}.\n\n'
'Changes included in this release:\n\n' +
'\n'.join(' ' + line for line in log.splitlines()))
body += (f'\n\nThe release is available under the {version} and yocto-{version} tags.\n'
'Both tags are GPG signed.\n\nThanks,\nThe meta-arm team')
return subject, body
@staticmethod
def editor_command():
# git var GIT_EDITOR already respects GIT_EDITOR, VISUAL, EDITOR and core.editor.
result = git('var', 'GIT_EDITOR', check=False)
return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else 'vi'
def edit_message(self, subject, body):
original = f'Subject: {subject}\n\n{body}\n'
with tempfile.TemporaryDirectory(prefix='meta-arm-release-') as directory:
path = Path(directory) / 'announcement.txt'
path.write_text(original, encoding='utf-8')
editor = self.editor_command()
print(f'Opening announcement in {editor}...', flush=True)
# Interactive editors must inherit stdin, stdout, and stderr.
# Git editor settings may contain shell quoting and arguments.
try:
subprocess.run(
['sh', '-c', f'{editor} "$1"', 'editor', str(path)],
check=True,
)
except subprocess.CalledProcessError as exc:
raise ReleaseError('Editor exited unsuccessfully; email not sent') from exc
edited = path.read_text(encoding='utf-8')
if edited == original:
print('Email unchanged; not sending.')
return None
header, separator, text = edited.partition('\n\n')
if not separator or not header.startswith('Subject: '):
raise ReleaseError('Edited email must begin with Subject: followed by a blank line')
new_subject = header.removeprefix('Subject: ').strip()
if not new_subject or '\r' in new_subject or '\n' in new_subject or not text.strip():
raise ReleaseError('Invalid edited email subject or body')
return new_subject, text.rstrip('\n')
def mail_backend(self):
"""Resolve the requested mailer without silently changing forced choices."""
sendmail = shutil.which('sendmail')
if not sendmail and os.access('/usr/sbin/sendmail', os.X_OK):
sendmail = '/usr/sbin/sendmail'
mutt = shutil.which('mutt')
if self.mailer == 'sendmail':
return sendmail
if self.mailer == 'mutt':
return mutt
return sendmail or mutt
def send(self, subject, body):
backend = self.mail_backend()
if not backend:
raise ReleaseError(f'Requested mailer {self.mailer!r} is unavailable' if self.mailer != 'auto'
else 'Neither sendmail nor mutt is available')
if os.path.basename(backend) == 'mutt':
subprocess.run([backend, '-s', subject, '--', self.recipient],
input=body + '\n', text=True, check=True)
else:
message = EmailMessage()
message['To'] = self.recipient
message['Subject'] = subject
message.set_content(body)
subprocess.run([backend, '-t', '-i'], input=message.as_bytes(), check=True)
def email(self, branch, version):
subject, body = self.announcement(branch, version)
if self.edit:
changed = self.edit_message(subject, body)
if changed is None:
return
subject, body = changed
print(f'\nSubject: {subject}\n\n{body}\n')
if not self.execute:
print('DRY RUN: Email not sent.')
return
for tag in (version, f'yocto-{version}'):
output = git('ls-remote', '--tags', '--refs', self.remote, f'refs/tags/{tag}').stdout
entries = dict(line.split()[::-1] for line in output.splitlines())
if entries.get(f'refs/tags/{tag}') != self.tag_object(tag):
raise ReleaseError(f'{tag} is not published identically on {self.remote}')
self.send(subject, body)
print('Email submitted successfully.')
def check(self):
print('meta-arm release environment\n' + '-' * 48)
errors = 0
inside = git('rev-parse', '--is-inside-work-tree', check=False).returncode == 0
print(f'Git repository: {"OK" if inside else "FAILED"}')
errors += not inside
url = git('remote', 'get-url', self.remote, check=False)
print(f'Upstream remote: {self.remote} ({url.stdout.strip() or "missing"})')
reachable = url.returncode == 0 and git('ls-remote', '--heads', self.remote, check=False).returncode == 0
print(f'Upstream reachable: {"OK" if reachable else "FAILED"}')
errors += not reachable
master = git('rev-parse', '--verify', '-q', f'{self.branch_ref("master")}^{{commit}}', check=False).returncode == 0
print(f'Master branch: {"OK" if master else "MISSING"}')
errors += not master
if master:
print(f'Release branches: {len(self.branches())}')
identity = (git('config', 'user.name', check=False).stdout.strip(),
git('config', 'user.email', check=False).stdout.strip())
print(f'Git identity: {identity[0]} <{identity[1]}>' if all(identity)
else 'Git identity: NOT CONFIGURED')
errors += not all(identity)
signing = git('config', 'user.signingkey', check=False).stdout.strip()
print(f'GPG signing key: {signing or "Git default (not verified)"}')
mailer_path = self.mail_backend()
mailer_note = ' (forced)' if self.mailer != 'auto' else ' (auto)'
print(f'Email delivery: {(mailer_path or "Not available") + mailer_note}')
print(f'Email recipient: {self.recipient}')
print(f'Editor: {self.editor_command()}')
print(f'LTS branches: {", ".join(self.lts)}')
print('Environment ready.' if not errors else f'Environment check failed: {errors} error(s).')
return not errors
class FriendlyArgumentParser(argparse.ArgumentParser):
"""Display the curated usage page without argparse's duplicate sections."""
def format_help(self):
return self.description + '\n'
def parser():
"""Build a CLI with a compact, example-driven help page."""
help_text = """\
Usage: meta-arm-release.py COMMAND [ARGUMENTS] [OPTIONS]
Commands:
check Check the release environment
status [BRANCH...] Show release status and pending commits
release BRANCH VERSION Create initial signed release tags (x.y)
update [BRANCH...] Tag branches containing new commits (x.y.z)
tag BRANCH VERSION REF Tag a specific historical commit
platform NAME REF Create a signed platform release tag
push [TAG|BRANCH...] Publish missing release tags to upstream
email BRANCH VERSION Preview or send an announcement
all BRANCH VERSION Initial release, updates, push and announcements
Options:
-h, --help, -? Display this help
-n, --dry-run Preview operations (default)
-x, --execute Perform requested operations
-e, --edit Edit announcements using your configured editor
--lts Treat specified release as LTS
--as-of YYYY-MM-DD Select historical first-parent commit by date
--date YYYY.MM.DD Platform tag date (default: today)
--remote NAME Upstream remote (default: yoctoproject)
--email-to ADDRESS Override announcement recipient
--mailer {auto,sendmail,mutt} Select email delivery method (default: auto)
--lts-branches "A B" Set supported LTS branches
Examples:
meta-arm-release.py check
meta-arm-release.py status
meta-arm-release.py status scarthgap wrynose
meta-arm-release.py release blacksail 6.1
meta-arm-release.py release blacksail 6.1 --execute
meta-arm-release.py update scarthgap whinlatter --execute
meta-arm-release.py tag wrynose 6.0 COMMIT --execute
meta-arm-release.py platform corstone1000 HEAD
meta-arm-release.py platform corstone1000 HEAD --date 2026.10.01 --execute
meta-arm-release.py push CORSTONE1000-2026.10.01 --execute
meta-arm-release.py push
meta-arm-release.py push --execute
meta-arm-release.py email blacksail 6.1
meta-arm-release.py email blacksail 6.1 --edit
meta-arm-release.py email blacksail 6.1 --edit --execute
meta-arm-release.py email blacksail 6.1 --mailer mutt --execute
meta-arm-release.py all blacksail 6.1 --execute
Notes:
* Dry-run is the default; --execute does not ask for confirmation.
* Email previews require no mail program; automatic delivery prefers
sendmail and falls back to mutt. Use --mailer to force a backend.
* Editor selection follows Git configuration (including VISUAL/EDITOR).
* Release branches must exist upstream; branches containing '/' are skipped.
* Existing tags are never overwritten; only missing tags are pushed.
* Push without arguments discovers Yocto and platform release tags.
* Push operations use only the configured upstream remote.
* The script never checks out branches.
Environment variables:
META_ARM_RELEASE_REMOTE Override upstream remote
META_ARM_RELEASE_EMAIL Override announcement recipient
META_ARM_LTS_BRANCHES Space-separated LTS branches
META_ARM_MAILER auto, sendmail, or mutt
"""
p = FriendlyArgumentParser(
prog='meta-arm-release.py',
description=help_text,
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
p.add_argument('-h', '--help', '-?', action='help', help=argparse.SUPPRESS)
p.add_argument('--execute', '-x', action='store_true', help=argparse.SUPPRESS)
p.add_argument('--dry-run', '-n', action='store_true', help=argparse.SUPPRESS)
p.add_argument('--edit', '-e', action='store_true', help=argparse.SUPPRESS)
p.add_argument('--lts', action='store_true', help=argparse.SUPPRESS)
p.add_argument('--as-of', metavar='YYYY-MM-DD', help=argparse.SUPPRESS)
p.add_argument('--date', metavar='YYYY.MM.DD', help=argparse.SUPPRESS)
p.add_argument('--remote', default=os.getenv('META_ARM_RELEASE_REMOTE', 'yoctoproject'),
help=argparse.SUPPRESS)
p.add_argument('--email-to', default=os.getenv('META_ARM_RELEASE_EMAIL', 'meta-arm@lists.yoctoproject.org'),
help=argparse.SUPPRESS)
p.add_argument('--mailer', choices=('auto', 'sendmail', 'mutt'),
default=os.getenv('META_ARM_MAILER', 'auto'), help=argparse.SUPPRESS)
p.add_argument('--lts-branches', default=os.getenv('META_ARM_LTS_BRANCHES', 'scarthgap wrynose'),
help=argparse.SUPPRESS)
sub = p.add_subparsers(dest='command', required=True, metavar='COMMAND')
sub.add_parser('check', add_help=False)
q = sub.add_parser('status', add_help=False)
q.add_argument('branches', nargs='*')
q = sub.add_parser('release', add_help=False)
q.add_argument('branch')
q.add_argument('version')
q = sub.add_parser('update', add_help=False)
q.add_argument('branches', nargs='*')
q = sub.add_parser('tag', add_help=False)
q.add_argument('branch')
q.add_argument('version')
q.add_argument('ref')
q = sub.add_parser('platform', add_help=False)
q.add_argument('name')
q.add_argument('ref')
q = sub.add_parser('push', add_help=False)
q.add_argument('selections', nargs='*')
q = sub.add_parser('email', add_help=False)
q.add_argument('branch')
q.add_argument('version')
q = sub.add_parser('all', add_help=False)
q.add_argument('branch')
q.add_argument('version')
return p
def main(argv=None):
p = parser()
# Support global options either before or after the subcommand.
args = list(sys.argv[1:] if argv is None else argv)
if not args or any(token in ('-h', '--help', '-?') for token in args):
p.print_help()
return 0
globals_with_value = ('--as-of', '--date', '--remote', '--email-to', '--lts-branches', '--mailer')
globals_flags = ('--execute', '-x', '--dry-run', '-n', '--edit', '-e', '--lts')
front, rest = [], []
i = 0
while i < len(args):
token = args[i]
if token in globals_with_value:
if i + 1 >= len(args):
p.error(f'{token} requires a value')
front.extend(args[i:i+2]); i += 2
elif token in globals_flags:
front.append(token); i += 1
else:
rest.append(token); i += 1
ns = p.parse_args(front + rest)
if ns.email_to.find('\n') >= 0 or ns.email_to.find('\r') >= 0:
p.error('Invalid email recipient')
if ns.date and ns.command != 'platform':
p.error('--date only applies to platform')
if ns.edit and ns.command not in ('email', 'all'):
p.error('--edit only applies to email and all')
if ns.as_of and not re.fullmatch(r'\d{4}-\d{2}-\d{2}', ns.as_of):
p.error('--as-of requires YYYY-MM-DD')
manager = ReleaseManager(ns.remote, ns.email_to, ns.lts_branches.split(),
ns.execute and not ns.dry_run, ns.edit, ns.as_of, ns.lts, ns.mailer)
if ns.command == 'check':
return 0 if manager.check() else 1
if git('rev-parse', '--is-inside-work-tree', check=False).returncode:
raise ReleaseError('Not in a Git repository')
if ns.command == 'status':
manager.show_status(ns.branches or manager.branches())
elif ns.command == 'release':
manager.release(ns.branch, ns.version)
elif ns.command == 'update':
manager.update(ns.branches or manager.branches())
elif ns.command == 'tag':
manager.require_branch(ns.branch)
expected = manager.discover_series()[ns.branch]
if ns.version != expected and not re.fullmatch(re.escape(expected) + r'\.\d+', ns.version):
raise ReleaseError(f'Wrong version series for {ns.branch}: {ns.version}')
manager.create_tags(ns.branch, ns.version, ns.ref, 'Release')
elif ns.command == 'platform':
manager.platform(ns.name, ns.ref, ns.date)
elif ns.command == 'push':
manager.push(ns.selections)
elif ns.command == 'email':
manager.email(ns.branch, ns.version)
elif ns.command == 'all':
manager.release(ns.branch, ns.version)
created = [(ns.branch, ns.version)]
created += manager.update([b for b in manager.branches() if b != ns.branch])
if manager.execute:
manager.push()
for branch, version in created:
manager.email(branch, version)
else:
print('DRY RUN: No tags created, pushed or emails sent.')
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except (ReleaseError, subprocess.CalledProcessError) as error:
print(f'ERROR: {error}', file=sys.stderr)
sys.exit(1)
-181
View File
@@ -1,181 +0,0 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Arm Limited
"""Report the age, change history, and upstream status of tracked patches."""
import argparse
import csv
import datetime as dt
import re
import subprocess
import sys
from collections import Counter, defaultdict
from pathlib import Path
def git(root, *args):
result = subprocess.run(['git', '-C', str(root), *args], capture_output=True)
if result.returncode:
raise RuntimeError(result.stderr.decode('utf-8', 'replace').strip())
return result.stdout
def metadata(root, path):
raw = git(root, 'log', '--follow', '--format=%as', '--', path)
dates = [x for x in raw.decode().splitlines() if x]
if not dates:
return '', '', 0
return dates[-1], dates[0], len(dates)
def upstream_status(path):
try:
content = path.read_text(encoding='utf-8', errors='replace')
except OSError as exc:
print(f'warning: cannot read {path}: {exc}', file=sys.stderr)
return 'Missing', ''
match = re.search(r'^\s*Upstream-Status:\s*(.*?)\s*$', content, re.I | re.M)
if not match:
return 'Missing', ''
full = match.group(1)
token = re.match(r'([^\s\[:]+)', full)
return (token.group(1) if token else 'Unknown'), full
def layer_of(path):
return next((p for p in path.parts if p.startswith('meta-')), '(root)')
def component_of(path):
# For standard recipes-*/<component>/files/foo.patch, choose <component>.
parts = path.parts
for i, part in enumerate(parts[:-1]):
if part.startswith('recipes-') and i + 1 < len(parts) - 1:
return parts[i + 1]
return path.parent.name
def main():
parser = argparse.ArgumentParser(
description='Report Git patch age and upstreaming status from a meta-arm checkout.',
epilog='''Examples:
scripts/patch-history.py
scripts/patch-history.py --verbose --sort age
scripts/patch-history.py --verbose --sort status
scripts/patch-history.py --layer meta-arm-bsp
scripts/patch-history.py --output bsp-patches.csv
Only Git-tracked .patch and .diff files are scanned. Git rename history is
followed where possible. Ages are calculated from the oldest and newest
commits touching each file, not from patch header dates. By default, a
summary is printed to the terminal. --verbose also lists each patch and
its age. --output writes CSV for spreadsheet use; otherwise no file is
created. No files in the tree are changed.''',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('--layer', help='Restrict scanning to this layer/directory relative to repository root')
parser.add_argument('--output', '-o', help='Write CSV to this path (no CSV is written by default)')
parser.add_argument('--sort', choices=['path', 'age', 'updated', 'component', 'status'], default='path',
help='Patch listing and CSV row order (status: grouped alphabetically, oldest first within each status; age and updated: oldest first)')
parser.add_argument('--summary', action='store_true', help='Print aggregate statistics (already the default; retained for compatibility)')
parser.add_argument('--verbose', '-v', action='store_true', help='List every patch with its age and upstream status in the terminal')
args = parser.parse_args()
try:
root = Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], stderr=subprocess.PIPE,
text=True).strip()).resolve()
except subprocess.CalledProcessError:
parser.error('run this from inside a Git repository')
if args.layer:
target = (root / args.layer).resolve()
if not target.is_relative_to(root) or not target.is_dir():
parser.error('--layer must name an existing directory inside the repository')
scope = target.relative_to(root).as_posix()
else:
scope = None
try:
files = git(root, 'ls-files', '-z', '--', *( [scope] if scope else [] )).split(b'\0')
except RuntimeError as exc:
parser.error(str(exc))
today = dt.date.today()
rows = []
for encoded in files:
if not encoded:
continue
relative = encoded.decode('utf-8', 'surrogateescape')
if Path(relative).suffix.lower() not in ('.patch', '.diff'):
continue
absolute = root / relative
if not absolute.is_file():
continue
try:
added, updated, changes = metadata(root, relative)
except RuntimeError as exc:
print(f'warning: history unavailable for {relative}: {exc}', file=sys.stderr)
added, updated, changes = '', '', 0
status, detail = upstream_status(absolute)
rows.append({
'Patch Added': added,
'Last Updated': updated,
'Times Changed': changes,
'Component': component_of(Path(relative)),
'Upstream Status': status,
'Patch Age (Days)': (today - dt.date.fromisoformat(added)).days if added else '',
'Days Since Update': (today - dt.date.fromisoformat(updated)).days if updated else '',
'Layer': layer_of(Path(relative)),
'Patch File': relative,
'Upstream Status Detail': detail,
})
keys = {
'path': lambda r: r['Patch File'],
'age': lambda r: (-(r['Patch Age (Days)'] if isinstance(r['Patch Age (Days)'], int) else -1), r['Patch File']),
'updated': lambda r: (-(r['Days Since Update'] if isinstance(r['Days Since Update'], int) else -1), r['Patch File']),
'component': lambda r: (r['Component'], r['Patch File']),
'status': lambda r: (r['Upstream Status'].lower(), -(r['Patch Age (Days)'] if isinstance(r['Patch Age (Days)'], int) else -1), r['Patch File']),
}
rows.sort(key=keys[args.sort])
if args.output:
output = Path(args.output)
fields = ['Patch Added', 'Last Updated', 'Times Changed', 'Component', 'Upstream Status',
'Patch Age (Days)', 'Days Since Update', 'Layer', 'Patch File', 'Upstream Status Detail']
try:
with output.open('w', newline='', encoding='utf-8') as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
except OSError as exc:
parser.exit(1, f'error: cannot write {output}: {exc}\n')
print(f'Wrote {len(rows)} patches to {output}')
if args.verbose:
print('\nPatch Details (age in days)')
print('===========================')
for row in rows:
age = row['Patch Age (Days)']
print(f"{str(age) if age != '' else 'n/a':>7} {row['Upstream Status']:<16} {row['Patch File']}")
if True: # Summary is the default terminal output.
ages = [r['Patch Age (Days)'] for r in rows if isinstance(r['Patch Age (Days)'], int)]
print('\nPatch Maintenance Summary')
print('=========================')
print(f'Total patches: {len(rows)}')
if ages:
print(f'Average patch age: {sum(ages) / len(ages):.0f} days')
print(f'Oldest patch: {max(ages)} days')
print(f'Patches older than 1 year: {sum(a > 365 for a in ages)}')
print(f'Patches older than 2 years: {sum(a > 730 for a in ages)}')
counts = Counter(r['Upstream Status'] for r in rows)
groups = defaultdict(list)
for r in rows:
if isinstance(r['Patch Age (Days)'], int):
groups[r['Upstream Status']].append(r['Patch Age (Days)'])
print('\nUpstream Status (count, average age in days):')
for status in sorted(counts, key=str.lower):
ages_for_status = groups[status]
average = f'{sum(ages_for_status) / len(ages_for_status):.0f}' if ages_for_status else 'n/a'
print(f' {status:<20} {counts[status]:>5} {average:>6}')
if __name__ == '__main__':
main()
-220
View File
@@ -1,220 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Arm Limited
# SPDX-License-Identifier: MIT
"""Offline unit and integration tests for meta-arm-release.py."""
import importlib.util
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SCRIPT = Path(__file__).with_name('meta-arm-release.py')
spec = importlib.util.spec_from_file_location('meta_arm_release', SCRIPT)
release = importlib.util.module_from_spec(spec)
import sys
sys.modules[spec.name] = release
spec.loader.exec_module(release)
class VersionTests(unittest.TestCase):
def test_initial_release_sequence(self):
self.assertEqual(release.previous_series('6.0'), '5.3')
self.assertEqual(release.previous_series('6.1'), '6.0')
self.assertEqual(release.series_from_number(release.series_number('5.3') + 1), '6.0')
def test_patch_version(self):
self.assertEqual(release.next_patch('5.0'), '5.0.1')
self.assertEqual(release.next_patch('5.0.4'), '5.0.5')
def test_bad_version(self):
with self.assertRaises(release.ReleaseError):
release.series_number('6.4')
class GitFixture(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.repo = Path(self.temp.name)
self.previous = Path.cwd()
os.chdir(self.repo)
self.addCleanup(os.chdir, self.previous)
self.git('init', '-q', '-b', 'master')
self.git('config', 'user.name', 'Test Maintainer')
self.git('config', 'user.email', 'test@example.com')
self.commit('initial')
self.remote = self.repo / 'upstream.git'
subprocess.check_call(['git', 'init', '-q', '--bare', str(self.remote)])
self.git('remote', 'add', 'yoctoproject', str(self.remote))
self.git('update-ref', 'refs/remotes/yoctoproject/master', self.git('rev-parse', 'master'))
def git(self, *args):
return subprocess.check_output(['git', *args], text=True).strip()
def commit(self, subject):
with (self.repo / 'history.txt').open('a') as stream:
stream.write(subject + '\n')
self.git('add', 'history.txt')
self.git('commit', '-q', '-m', subject)
def branch(self, name):
self.git('branch', name)
self.git('update-ref', f'refs/remotes/yoctoproject/{name}', self.git('rev-parse', name))
def refresh(self, name):
self.git('update-ref', f'refs/remotes/yoctoproject/{name}', self.git('rev-parse', name))
class ReleaseIntegrationTests(GitFixture):
def test_existing_tag_and_update(self):
self.branch('scarthgap')
self.git('checkout', '-q', 'scarthgap')
self.git('tag', '-a', '5.0', '-m', 'initial')
self.git('tag', '-a', 'yocto-5.0', '-m', 'initial')
self.commit('fix one')
self.commit('fix two')
self.refresh('scarthgap')
self.git('checkout', '-q', 'master')
manager = release.ReleaseManager()
state = manager.status('scarthgap')
self.assertEqual((state.series, state.last_tag, state.commits, state.next_tag),
('5.0', '5.0', 2, '5.0.1'))
def test_no_updates(self):
self.branch('scarthgap')
self.git('tag', '-a', '5.0', '-m', 'initial')
self.git('tag', '-a', 'yocto-5.0', '-m', 'initial')
state = release.ReleaseManager().status('scarthgap')
self.assertEqual(state.commits, 0)
self.assertEqual(state.next_tag, '-')
def test_announcement_initial_count(self):
self.branch('wrynose')
self.git('tag', '-a', '6.0', '-m', 'initial')
self.git('tag', '-a', 'yocto-6.0', '-m', 'initial')
self.commit('master change')
self.branch('blacksail')
self.git('tag', '-a', '6.1', '-m', 'initial')
self.git('tag', '-a', 'yocto-6.1', '-m', 'initial')
manager = release.ReleaseManager()
subject, body = manager.announcement('blacksail', '6.1')
self.assertEqual(subject, 'meta-arm blacksail release')
self.assertIn('1 commits since the previous Yocto Project release, 6.0', body)
self.assertIn('The meta-arm team', body)
self.assertNotIn('Changes included', body)
def test_update_announcement_lists_changes(self):
self.branch('scarthgap')
self.git('checkout', '-q', 'scarthgap')
self.git('tag', '-a', '5.0', '-m', 'initial')
self.git('tag', '-a', 'yocto-5.0', '-m', 'initial')
self.commit('fix one')
self.git('tag', '-a', '5.0.1', '-m', 'update')
self.git('tag', '-a', 'yocto-5.0.1', '-m', 'update')
self.refresh('scarthgap')
manager = release.ReleaseManager()
subject, body = manager.announcement('scarthgap', '5.0.1')
self.assertIn('fix one', body)
self.assertIn('1 commits', body)
self.assertIn('5.0.1 and yocto-5.0.1 tags', body)
def test_push_dry_run_does_not_modify_remote(self):
self.branch('scarthgap')
self.git('tag', '-a', '5.0', '-m', 'initial')
self.git('tag', '-a', 'yocto-5.0', '-m', 'initial')
manager = release.ReleaseManager(execute=False)
with patch('builtins.print') as printer:
manager.push(['scarthgap'])
self.assertTrue(any('New tags: 2' in str(call) for call in printer.call_args_list))
class MailerTests(unittest.TestCase):
def test_auto_prefers_sendmail(self):
with patch.object(release.shutil, 'which', side_effect=lambda x: {'sendmail': '/usr/bin/sendmail', 'mutt': '/usr/bin/mutt'}.get(x)):
self.assertEqual(release.ReleaseManager(mailer='auto').mail_backend(), '/usr/bin/sendmail')
def test_force_mutt(self):
with patch.object(release.shutil, 'which', side_effect=lambda x: {'sendmail': '/usr/bin/sendmail', 'mutt': '/usr/bin/mutt'}.get(x)):
self.assertEqual(release.ReleaseManager(mailer='mutt').mail_backend(), '/usr/bin/mutt')
def test_force_sendmail_unavailable_does_not_fallback(self):
with patch.object(release.shutil, 'which', side_effect=lambda x: {'mutt': '/usr/bin/mutt'}.get(x)), patch.object(release.os, 'access', return_value=False):
manager = release.ReleaseManager(mailer='sendmail')
self.assertIsNone(manager.mail_backend())
with self.assertRaisesRegex(release.ReleaseError, 'sendmail'):
manager.send('subject', 'body')
def test_cli_override_env(self):
with patch.dict(os.environ, {'META_ARM_MAILER': 'mutt'}):
self.assertEqual(release.parser().parse_args(['--mailer', 'sendmail', 'check']).mailer, 'sendmail')
self.assertEqual(release.parser().parse_args(['check']).mailer, 'mutt')
def test_invalid_mailer_rejected(self):
with self.assertRaises(release.ReleaseError):
release.ReleaseManager(mailer='invalid')
if __name__ == '__main__':
unittest.main(verbosity=2)
class EditorTests(unittest.TestCase):
def test_editor_inherits_terminal_streams(self):
manager = release.ReleaseManager(edit=True)
with patch.object(manager, 'editor_command', return_value='vim -c "set spell"'):
with patch.object(release.subprocess, 'run') as invoke:
result = manager.edit_message('Subject', 'Body')
self.assertIsNone(result)
invoke.assert_called_once()
args, kwargs = invoke.call_args
self.assertEqual(args[0][:3], ['sh', '-c', 'vim -c "set spell" "$1"'])
self.assertTrue(kwargs['check'])
self.assertNotIn('stdout', kwargs)
self.assertNotIn('stderr', kwargs)
self.assertNotIn('stdin', kwargs)
def test_editor_failure_prevents_send(self):
manager = release.ReleaseManager(edit=True)
with patch.object(manager, 'editor_command', return_value='vim'):
with patch.object(release.subprocess, 'run', side_effect=subprocess.CalledProcessError(1, 'vim')):
with self.assertRaisesRegex(release.ReleaseError, 'Editor exited unsuccessfully'):
manager.edit_message('Subject', 'Body')
class PlatformReleaseTests(GitFixture):
def test_platform_tag_name_and_date(self):
self.assertEqual(release.platform_tag('corstone1000', '2026.10.01'),
'CORSTONE1000-2026.10.01')
with self.assertRaises(release.ReleaseError):
release.platform_tag('corstone1000', '2026.02.30')
def test_platform_dry_run_does_not_create_tag(self):
manager = release.ReleaseManager()
tag = manager.platform('corstone1000', 'HEAD', '2026.10.01')
self.assertEqual(tag, 'CORSTONE1000-2026.10.01')
self.assertIsNone(manager.tag_object(tag))
def test_platform_existing_tag_is_idempotent(self):
self.git('tag', '-a', 'CORSTONE1000-2026.10.01', '-m', 'release')
manager = release.ReleaseManager(execute=True)
self.assertEqual(manager.platform('corstone1000', 'HEAD', '2026.10.01'),
'CORSTONE1000-2026.10.01')
def test_push_explicit_platform_tag(self):
self.git('tag', '-a', 'CORSTONE1000-2026.10.01', '-m', 'release')
manager = release.ReleaseManager(execute=True)
manager.push(('CORSTONE1000-2026.10.01',))
remote = self.git('ls-remote', '--tags', '--refs', 'yoctoproject',
'refs/tags/CORSTONE1000-2026.10.01')
self.assertIn('refs/tags/CORSTONE1000-2026.10.01', remote)
def test_push_auto_discovers_platform_tag(self):
self.git('tag', '-a', 'CORSTONE1000-2026.10.01', '-m', 'release')
manager = release.ReleaseManager(execute=True)
manager.push()
remote = self.git('ls-remote', '--tags', '--refs', 'yoctoproject',
'refs/tags/CORSTONE1000-2026.10.01')
self.assertIn('refs/tags/CORSTONE1000-2026.10.01', remote)