add petalinux qspi transition workflow
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
name: petalinux-qspi
|
||||||
|
description: Build, reuse, deploy, recover, and hardware-validate a PetaLinux QSPI transition system from a user-supplied Vivado HDF/XSA and board information. Use when the user asks to create or install a QSPI rescue/transition Linux, update BOOT.BIN or image.ub in QSPI, prepare removable storage from that Linux, or determine whether an existing QSPI delivery is current. Supports a validated HJX9009 profile and intake routes for emv9.3, ipots, and other boards.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PetaLinux QSPI transition system
|
||||||
|
|
||||||
|
Create a small, recoverable Linux in QSPI that can bring up networking, diagnose storage, and prepare the later production system. Reuse a delivery only when its inputs and artifacts are provably identical; deployment and hardware acceptance are always repeated for the target board.
|
||||||
|
|
||||||
|
## 1. Select the board first
|
||||||
|
|
||||||
|
If the user has not already named the board, ask exactly this choice before inspecting or changing a project:
|
||||||
|
|
||||||
|
1. HJX9009
|
||||||
|
2. emv9.3
|
||||||
|
3. ipots
|
||||||
|
4. others
|
||||||
|
|
||||||
|
- For **HJX9009**, read [references/hjx9009.md](references/hjx9009.md) before planning or editing.
|
||||||
|
- For **emv9.3** or **ipots**, the electrical/deployment profile is not implemented yet. Read [references/board-intake.md](references/board-intake.md), collect and verify its inputs, then create a board reference before any QSPI write. Never borrow HJX9009 constants.
|
||||||
|
- For **others**, require the hardware details in `board-intake.md`. A schematic, Vivado configuration report, or vendor board file is required; stop before build/deployment if boot straps, flash topology, or storage wiring remains unknown.
|
||||||
|
|
||||||
|
## 2. Establish inputs and authority
|
||||||
|
|
||||||
|
Read the applicable project `AGENTS.md`/`CLAUDE.md` and bmem project/machine memory before substantive work. Machine paths, IP addresses, serial ports, relay mappings, tool locations, usernames, and credentials come from live discovery, bmem pointers, or `~\skills_env.md`; never put them in this skill or copy values from an old note.
|
||||||
|
|
||||||
|
Require or locate:
|
||||||
|
|
||||||
|
- user-supplied Vivado hardware handoff (`.hdf` for PetaLinux 2018.1-era flows, `.xsa` for newer flows), plus bitstream only when the requested BOOT composition needs it;
|
||||||
|
- matching Vivado/SDK/PetaLinux versions; do not silently upgrade a hardware handoff;
|
||||||
|
- board schematic/configuration evidence and the exact target board identity;
|
||||||
|
- accessible serial console, power control, JTAG, and a proven recovery image/path before erasing QSPI;
|
||||||
|
- current host, VM, TFTP, and board network facts verified on the live interfaces.
|
||||||
|
|
||||||
|
Ask for the exact destructive target and show the selected board, image hashes, flash ranges, and recovery path immediately before the first QSPI erase/program operation if they were not already explicit in the user's deployment request.
|
||||||
|
|
||||||
|
## 3. Decide whether to rebuild
|
||||||
|
|
||||||
|
Read [references/cache-and-delivery.md](references/cache-and-delivery.md). Use `scripts/qspi_cache.py capture` to fingerprint all build-defining inputs and `scripts/qspi_cache.py check` to evaluate an existing delivery.
|
||||||
|
|
||||||
|
Skip the build only when all are true:
|
||||||
|
|
||||||
|
- board profile and its revision match;
|
||||||
|
- HDF/XSA and every included overlay/config/script hash match;
|
||||||
|
- PetaLinux, Vivado, SDK, boot composition, QSPI layout, and relevant parameters match;
|
||||||
|
- `BOOT.BIN`, `image.ub.qspi`, `flash_layout.json`, `summary.json`, and checksum manifest pass validation;
|
||||||
|
- prior build gates G0-G3 are PASS.
|
||||||
|
|
||||||
|
File existence, timestamps, an unchanged bitstream alone, or a prior successful boot are insufficient. A reusable build still goes through G4-G6 on the present hardware. If any fingerprint field is missing or differs, rebuild.
|
||||||
|
|
||||||
|
## 4. Execute the gated workflow
|
||||||
|
|
||||||
|
Read [references/workflow.md](references/workflow.md) and apply gates in order:
|
||||||
|
|
||||||
|
- **G0 preflight:** resolve inputs, live network, free space, serial/JTAG/power, QSPI recovery, and a no-write baseline.
|
||||||
|
- **G1 hardware import:** create a clean project and import the exact HDF/XSA. Archive generated configuration before overlays.
|
||||||
|
- **G2 configuration:** enable QSPI, initramfs, MTD, network, storage drivers, and required deployment/diagnostic tools. Audit generated device trees and bootloader configuration against the selected board reference.
|
||||||
|
- **G3 build/package:** build without hidden errors; package a deliberate BOOT composition; create hashes, input fingerprint, Flash layout, and manifest.
|
||||||
|
- **G4 boot-region deployment:** program/verify BOOT.BIN, then cold boot into the intended U-Boot. Preserve a recovery route.
|
||||||
|
- **G5 image deployment:** transfer, size-check, erase only the image region, write, full-readback, CRC/hash, and byte-compare `image.ub.qspi`. Save the exact QSPI-only `bootcmd` only after verification.
|
||||||
|
- **G6 system acceptance:** perform at least two full power-cycle boots; prove BootROM/U-Boot/FIT/Linux origin, MTD layout, networking, tools, and target storage behavior.
|
||||||
|
|
||||||
|
Never let a failed transfer fall through into erase/write commands. Use checked steps and stop on the first missing expected response or nonzero result. A successful build or `sf write` is not acceptance.
|
||||||
|
|
||||||
|
## 5. Storage initialization boundary
|
||||||
|
|
||||||
|
The transition system may contain partitioning and filesystem tools, but insertion does not authorize formatting.
|
||||||
|
|
||||||
|
1. Power off before inserting media when required by the board workflow.
|
||||||
|
2. Boot from QSPI and prove the QSPI path again.
|
||||||
|
3. Enumerate the target and read identity, CID, capacity, current mounts, partition table, and sample sectors without writing.
|
||||||
|
4. If no card is physically inserted, record `WAITING_FOR_MEDIA`, not a driver failure.
|
||||||
|
5. Format or deploy a production filesystem only after the target device is unambiguous and the user has requested that destructive step.
|
||||||
|
|
||||||
|
Never infer eMMC from a Linux name such as `/dev/mmcblk0`; it may be removable SD.
|
||||||
|
|
||||||
|
## 6. Evidence and handoff
|
||||||
|
|
||||||
|
- Put automatic logs/state under the project `Notes/auto/`, never in the `Notes/` root.
|
||||||
|
- Record exact artifact sizes, SHA-256, U-Boot CRC, flash offsets/lengths, bootcmd, live IPs, gate status, and evidence paths.
|
||||||
|
- Distinguish static inspection, VM build, flash write/readback, warm boot, cold boot, and real-media results.
|
||||||
|
- Do not claim completion while a required physical medium is absent; state the precise remaining gate.
|
||||||
|
- At substantive-work completion, import changed user-facing Notes and a session summary through bmem, add durable project decisions/issues at the correct layer, and run `bmem sync`. Report local/central/remote sync outcomes separately.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "PetaLinux QSPI"
|
||||||
|
short_description: "Build, reuse, deploy, and verify a QSPI rescue Linux"
|
||||||
|
default_prompt: "Use $petalinux-qspi to build or reuse and deploy a QSPI transition system for my selected board."
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Board intake for emv9.3, ipots, and other boards
|
||||||
|
|
||||||
|
Do not build or erase QSPI until the following facts are backed by a schematic, Vivado report, vendor board definition, or direct measurement.
|
||||||
|
|
||||||
|
## Required identity
|
||||||
|
|
||||||
|
- board name, PCB revision, serial/asset identity, SoC/FPGA exact part;
|
||||||
|
- Vivado hardware handoff and its tool version;
|
||||||
|
- expected PetaLinux/SDK version and whether migration is authorized;
|
||||||
|
- intended role of the transition system and later production storage.
|
||||||
|
|
||||||
|
## Boot and QSPI
|
||||||
|
|
||||||
|
- boot-mode strap/switch mapping, active levels, currently selected setting, and when it is sampled;
|
||||||
|
- flash manufacturer/part, single/dual, stacked/parallel/cascade topology, bus width, total bytes, page and erase sizes;
|
||||||
|
- PS/PL controller, chip-selects, MIO/pin mapping, voltage, reset and power dependencies;
|
||||||
|
- recovery method if the boot region becomes invalid;
|
||||||
|
- required BOOT.BIN contents: FSBL, U-Boot, optional bitstream/PMUFW/ATF, and why.
|
||||||
|
|
||||||
|
## Console and networking
|
||||||
|
|
||||||
|
- UART instance, pin routing, baud rate and physical COM adapter;
|
||||||
|
- Ethernet controller, PHY model/address/interface/reset/clock;
|
||||||
|
- live host/VM/board subnet, route, firewall and TFTP binding. Discover these each run; do not copy old addresses.
|
||||||
|
|
||||||
|
## Later storage
|
||||||
|
|
||||||
|
- storage type (removable SD, soldered eMMC, NAND, USB, other), controller instance, pin/MIO mapping and bus width;
|
||||||
|
- card-detect/write-protect/power-enable wiring and polarity;
|
||||||
|
- voltage switching capability and maximum safe frequency;
|
||||||
|
- expected Linux block-device evidence; never assume numbering before enumeration.
|
||||||
|
|
||||||
|
## Acceptance decisions
|
||||||
|
|
||||||
|
- exact QSPI layout with non-overlap and erase alignment;
|
||||||
|
- maximum image sizes and RAM load addresses;
|
||||||
|
- cold-boot count, network checks, storage checks, and whether blank-media formatting is part of this request;
|
||||||
|
- board-specific hazards and the stop/recovery threshold.
|
||||||
|
|
||||||
|
Create `references/<board-name>.md` only after these facts are verified. Mark every unverified item explicitly; placeholders are not deployment permission.
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Build reuse and delivery contract
|
||||||
|
|
||||||
|
## Input fingerprint
|
||||||
|
|
||||||
|
Fingerprint every item that can change generated boot behavior:
|
||||||
|
|
||||||
|
- selected board profile file;
|
||||||
|
- HDF/XSA and optional bitstream;
|
||||||
|
- project config, rootfs config, kernel config fragments, device-tree overlays, U-Boot overlays/BIF, recipes and custom files;
|
||||||
|
- PetaLinux, Vivado and SDK versions;
|
||||||
|
- BOOT.BIN composition;
|
||||||
|
- QSPI geometry/layout and boot parameters that are not machine-local.
|
||||||
|
|
||||||
|
Do not fingerprint credentials, transient logs, IP addresses discovered only for deployment, or build timestamps. Store the generated file as `build_inputs.json` in the delivery.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python <skill>\scripts\qspi_cache.py capture `
|
||||||
|
--board-profile <skill>\references\hjx9009.md `
|
||||||
|
--input HDF=<path-to-system.hdf> `
|
||||||
|
--input SYSTEM_USER_DTSI=<path-to-system-user.dtsi> `
|
||||||
|
--input ROOTFS_CONFIG=<path-to-rootfs-config> `
|
||||||
|
--value PETALINUX_VERSION=2018.1 `
|
||||||
|
--value VIVADO_VERSION=2018.1 `
|
||||||
|
--value BOOT_COMPOSITION=FSBL+U-Boot `
|
||||||
|
--output <work>\build_inputs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
After building, copy that exact `build_inputs.json` into the delivery and generate `SHA256SUMS` for the delivery artifacts. Do not include `SHA256SUMS` itself in that file.
|
||||||
|
|
||||||
|
## Reuse check
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python <skill>\scripts\qspi_cache.py check `
|
||||||
|
--current <work>\build_inputs.json `
|
||||||
|
--delivery <delivery-directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit 0 and `reusable: true` allow skipping G1-G3. Any other result requires rebuild or repair. Reuse never skips target-board programming/readback/cold-boot gates.
|
||||||
|
|
||||||
|
## Required delivery files
|
||||||
|
|
||||||
|
- `BOOT.BIN`
|
||||||
|
- `image.ub.qspi`
|
||||||
|
- `flash_layout.json`
|
||||||
|
- `summary.json`
|
||||||
|
- `build_inputs.json`
|
||||||
|
- `SHA256SUMS`
|
||||||
|
- human-readable manifest containing versions, composition, hashes, layout, bootcmd, gate states, known limitations and evidence paths
|
||||||
|
|
||||||
|
`flash_layout.json` must describe total/erase/page bytes, non-overlapping partitions, and the image file, offset, byte count, erase length and RAM load address. Image size must fit the named partition; offsets and erase lengths must align to the actual erase size.
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# HJX9009 validated board profile
|
||||||
|
|
||||||
|
This profile comes from the HJX-ADRV9009-X2 schematic and a PetaLinux 2018.1 hardware run. Reconfirm the PCB revision and live probes for every target; schematic constants do not replace target identity.
|
||||||
|
|
||||||
|
## Boot and QSPI
|
||||||
|
|
||||||
|
- SoC: XC7Z100FF900.
|
||||||
|
- Boot device straps are `MIO[5:4:3]`; `100` is QSPI and is the schematic default. The schematic implements resistor straps, not a user boot DIP. Boot mode is sampled at power-on reset.
|
||||||
|
- `MIO2=0` selects cascaded dual-QSPI mode by default.
|
||||||
|
- U5 and U6 are S25FL128S-family serial flashes. Validated U-Boot probe: `s25fl128s_64k`, 32 MiB total, 128 KiB erase, 512-byte reported page. Treat the live `sf probe` result as mandatory before erase.
|
||||||
|
- A TF card does not alter the QSPI boot selection. The transition U-Boot bootcmd must contain only `sf probe`, QSPI `sf read`, documented board fixups, and `bootm`; no implicit `mmc` fallback.
|
||||||
|
|
||||||
|
Validated transition layout:
|
||||||
|
|
||||||
|
| Region | Offset | Size |
|
||||||
|
|---|---:|---:|
|
||||||
|
| boot | `0x00000000` | `0x00500000` |
|
||||||
|
| bootenv | `0x00500000` | `0x00020000` |
|
||||||
|
| kernel | `0x00520000` | `0x01800000` |
|
||||||
|
| spare | `0x01D20000` | `0x002E0000` |
|
||||||
|
|
||||||
|
The transition `BOOT.BIN` contains FSBL and U-Boot only. It deliberately excludes the PL bitstream and `image.ub`. The kernel region stores a FIT `image.ub.qspi` containing kernel, DTB and initramfs rootfs. Calculate the actual image read and erase lengths for each build; never copy an old length.
|
||||||
|
|
||||||
|
## J1 microSD/TF interface
|
||||||
|
|
||||||
|
HJX9009 has no soldered eMMC. J1 is removable microSD through U12 MAX13035E:
|
||||||
|
|
||||||
|
- SD0 CLK/CMD/DAT0..3: MIO40..45;
|
||||||
|
- card detect `SDIO_SDDET`: MIO13;
|
||||||
|
- card side is 3.3 V and PS side is 1.8 V;
|
||||||
|
- no UHS 1.8 V card-side switching and no write-protect signal;
|
||||||
|
- MIO46..53 are occupied by Ethernet/UART/MDIO functions, so SD1 must remain disabled.
|
||||||
|
|
||||||
|
Required SD device-tree intent:
|
||||||
|
|
||||||
|
```dts
|
||||||
|
&sdhci0 {
|
||||||
|
status = "okay";
|
||||||
|
u-boot,dm-pre-reloc;
|
||||||
|
bus-width = <4>;
|
||||||
|
max-frequency = <50000000>;
|
||||||
|
xlnx,has-cd = <1>;
|
||||||
|
xlnx,has-power = <0>;
|
||||||
|
xlnx,has-wp = <0>;
|
||||||
|
no-1-8-v;
|
||||||
|
disable-wp;
|
||||||
|
};
|
||||||
|
|
||||||
|
&sdhci1 {
|
||||||
|
status = "disabled";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not add `non-removable`. On the validated Linux 4.14 native SDHCI path, card presence is read directly from the SDHCI Present State register; `cd-inverted` does not correct native detection and must not be used as a substitute for the proper CD selector.
|
||||||
|
|
||||||
|
## SD0 card-detect selector
|
||||||
|
|
||||||
|
The validated HDF enabled SD0 on MIO40..45 but left `PCW_SD0_GRP_CD_ENABLE=0`, so generated FSBL selected EMIO 56 instead of schematic MIO13. Inspect the new HDF and generated `ps7_init.c` every time:
|
||||||
|
|
||||||
|
- if generation selects MIO13 correctly, do not apply a workaround;
|
||||||
|
- otherwise prefer correcting the Vivado PS7 configuration/HDF and rebuilding FSBL;
|
||||||
|
- the validated transition-only fallback, applied immediately before `bootm`, is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sd0_cd_fix=mw.l 0xF8000008 0x0000DF0D; mw.l 0xF8000830 0x000D0037; mw.l 0xF8000004 0x0000767B
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `0xF8000830=0x000D0037` in Linux. With no card inserted, the validated SDHCI Present State at `0xE0100024` was `0x01FA0000` (Card Present bit clear). Absence of `/dev/mmcblk0` is expected only when the operator confirms no card is inserted.
|
||||||
|
|
||||||
|
## Network and boot details
|
||||||
|
|
||||||
|
- GEM0 uses an Atheros 8031 PHY at MDIO address 0 with `rgmii-id`.
|
||||||
|
- Discover MAC and IP values from the target/project; do not reuse a prior board address.
|
||||||
|
- The prior validated build needed a documented PHY register fix before networking. Re-evaluate it against the supplied HDF/U-Boot rather than applying it blindly.
|
||||||
|
- The initramfs may need `CONFIG_SYS_BOOTM_LEN=0x0F000000`; confirm against the generated FIT and available DDR.
|
||||||
|
|
||||||
|
## Acceptance markers
|
||||||
|
|
||||||
|
Cold-boot evidence must show, in order: U-Boot banner, `SF: Detected s25fl128s_64k`, exact `device 0 offset 0x520000, size <current-size>`, FIT loading, and the transition Linux login. In Linux require rootfs type `rootfs`, the four expected MTD regions, `mmc0` on `e0100000.sdhci`, live network/TFTP, and storage tools.
|
||||||
|
|
||||||
|
If no TF card is present, finish at `WAITING_FOR_MEDIA` and power off. After insertion, prove QSPI boot again, then identify `/dev/mmcblk0` and do read-only media checks before any partition or format operation.
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Gated build and deployment workflow
|
||||||
|
|
||||||
|
## G0 - preflight
|
||||||
|
|
||||||
|
1. Identify the project and target board; read its board reference.
|
||||||
|
2. Hash the user-provided HDF/XSA before copying it.
|
||||||
|
3. Verify matching tool versions and enough VM/build-disk space.
|
||||||
|
4. Discover live Windows/VM/board addresses and the TFTP interface. Prove both ping and actual file transfer when available.
|
||||||
|
5. Capture serial boot output, current environment, QSPI probe, boot mode evidence, MTD layout and current storage without writing.
|
||||||
|
6. Verify JTAG/power control and a tested recovery method. Stop before erase if recovery is not credible.
|
||||||
|
7. Capture the build input fingerprint and run the reuse check.
|
||||||
|
|
||||||
|
## G1-G3 - build or verified reuse
|
||||||
|
|
||||||
|
For a rebuild, create a clean project, import the exact handoff, archive the generated baseline, then apply only reviewed board overlays. Configure an initramfs root filesystem and include at least:
|
||||||
|
|
||||||
|
- QSPI/MTD and board Ethernet/PHY support;
|
||||||
|
- selected SD/eMMC host support;
|
||||||
|
- `fdisk` or `parted`, `blkid`, FAT formatting, ext4 formatting/checking, `mount`, `umount`, archive, checksum and TFTP tools;
|
||||||
|
- enough `CONFIG_SYS_BOOTM_LEN` for the uncompressed initramfs.
|
||||||
|
|
||||||
|
Build and package with logs and exit codes. Treat warnings about a detected flash differing from the device-tree expectation as unresolved board-profile drift even if access currently works.
|
||||||
|
|
||||||
|
Before deployment validate:
|
||||||
|
|
||||||
|
- generated DT contains the intended controller nodes and no conflicting alternate controller;
|
||||||
|
- BOOT.BIN contains exactly the declared components;
|
||||||
|
- image size fits its partition and DDR load range;
|
||||||
|
- `qspi_cache.py check` or equivalent delivery validation passes;
|
||||||
|
- all artifacts have stable size and SHA-256.
|
||||||
|
|
||||||
|
## G4 - boot region
|
||||||
|
|
||||||
|
Back up the known-good boot image when possible. Prefer the vendor-supported flash programmer with verify. If it fails, use a board-proven JTAG-to-DDR recovery path only after checking its address, size and marker protocol. Never improvise a recovery write on an unverified board.
|
||||||
|
|
||||||
|
After programming, fully remove and restore power. Confirm the intended U-Boot banner and QSPI probe. Repeat the cold boot; do not count a warm reset.
|
||||||
|
|
||||||
|
## G5 - image region
|
||||||
|
|
||||||
|
Use separate checked operations:
|
||||||
|
|
||||||
|
1. download the exact image and assert transferred byte count;
|
||||||
|
2. compute RAM CRC/hash and match the manifest;
|
||||||
|
3. probe QSPI and assert geometry;
|
||||||
|
4. erase `ceil(image_bytes / erase_bytes) * erase_bytes`, bounded by the image partition;
|
||||||
|
5. write exactly `image_bytes`;
|
||||||
|
6. read back exactly `image_bytes` to a second RAM address;
|
||||||
|
7. compare CRC/hash and every byte;
|
||||||
|
8. only then save the QSPI-only bootcmd.
|
||||||
|
|
||||||
|
The bootcmd must make the origin unambiguous: probe QSPI, read the declared offset/length, apply only documented board fixes, and boot that RAM address. Do not add silent SD/network fallback unless explicitly required.
|
||||||
|
|
||||||
|
## G6 - hardware acceptance
|
||||||
|
|
||||||
|
For at least two complete power cycles, retain console evidence of:
|
||||||
|
|
||||||
|
- BootROM mode/source evidence, QSPI probe, exact QSPI read offset/length and FIT load;
|
||||||
|
- Linux release, initramfs mounted as root, expected MTD partitions;
|
||||||
|
- board Ethernet/PHY identity, live address and real TFTP transfer;
|
||||||
|
- required storage host registration and device-tree/register evidence;
|
||||||
|
- required deployment tools.
|
||||||
|
|
||||||
|
With removable media inserted, first perform a read-only gate: device enumeration, CID, capacity, partition table, current mounts, and bounded sample reads. Formatting and production deployment are separate destructive phases.
|
||||||
|
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Capture build-defining inputs and validate reuse of a QSPI delivery."""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_ARTIFACTS = (
|
||||||
|
"BOOT.BIN", "image.ub.qspi", "flash_layout.json", "summary.json",
|
||||||
|
"build_inputs.json", "MANIFEST.md", "SHA256SUMS",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path):
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest().upper()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_assignments(values, option):
|
||||||
|
parsed = {}
|
||||||
|
for item in values:
|
||||||
|
if "=" not in item:
|
||||||
|
raise ValueError("{} requires NAME=VALUE: {}".format(option, item))
|
||||||
|
name, value = item.split("=", 1)
|
||||||
|
name = name.strip()
|
||||||
|
if not name or name in parsed:
|
||||||
|
raise ValueError("invalid or duplicate {} name: {}".format(option, name))
|
||||||
|
parsed[name] = value
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_fingerprint(payload):
|
||||||
|
base = dict(payload)
|
||||||
|
base.pop("fingerprint_sha256", None)
|
||||||
|
encoded = json.dumps(base, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||||
|
return hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper()
|
||||||
|
|
||||||
|
|
||||||
|
def capture(args):
|
||||||
|
profile = os.path.abspath(args.board_profile)
|
||||||
|
if not os.path.isfile(profile):
|
||||||
|
raise ValueError("board profile not found: {}".format(profile))
|
||||||
|
inputs = parse_assignments(args.input, "--input")
|
||||||
|
values = parse_assignments(args.value, "--value")
|
||||||
|
files = {}
|
||||||
|
for name in sorted(inputs):
|
||||||
|
path = os.path.abspath(inputs[name])
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
raise ValueError("input file not found for {}: {}".format(name, path))
|
||||||
|
files[name] = {
|
||||||
|
"path_hint": os.path.basename(path),
|
||||||
|
"bytes": os.path.getsize(path),
|
||||||
|
"sha256": sha256_file(path),
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"board_profile": {
|
||||||
|
"name": os.path.splitext(os.path.basename(profile))[0],
|
||||||
|
"sha256": sha256_file(profile),
|
||||||
|
},
|
||||||
|
"files": files,
|
||||||
|
"values": dict(sorted(values.items())),
|
||||||
|
}
|
||||||
|
payload["fingerprint_sha256"] = canonical_fingerprint(payload)
|
||||||
|
output = os.path.abspath(args.output)
|
||||||
|
parent = os.path.dirname(output)
|
||||||
|
if parent and not os.path.isdir(parent):
|
||||||
|
os.makedirs(parent)
|
||||||
|
with open(output, "w") as stream:
|
||||||
|
json.dump(payload, stream, indent=2, sort_keys=True)
|
||||||
|
stream.write("\n")
|
||||||
|
print(json.dumps({"captured": output, "fingerprint": payload["fingerprint_sha256"]}))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(path):
|
||||||
|
with open(path, "r") as stream:
|
||||||
|
return json.load(stream)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_checksums(path):
|
||||||
|
entries = {}
|
||||||
|
with open(path, "r") as stream:
|
||||||
|
for number, raw in enumerate(stream, 1):
|
||||||
|
line = raw.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(None, 1)
|
||||||
|
if len(parts) != 2 or len(parts[0]) != 64:
|
||||||
|
raise ValueError("invalid SHA256SUMS line {}".format(number))
|
||||||
|
name = parts[1].lstrip("* ")
|
||||||
|
pieces = name.replace("\\", "/").split("/")
|
||||||
|
if name == "SHA256SUMS" or os.path.isabs(name) or ".." in pieces:
|
||||||
|
raise ValueError("unsafe checksum entry: {}".format(name))
|
||||||
|
entries[name] = parts[0].upper()
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def validate_layout(delivery, errors):
|
||||||
|
try:
|
||||||
|
layout = read_json(os.path.join(delivery, "flash_layout.json"))
|
||||||
|
total = int(layout["flash"]["total_bytes"])
|
||||||
|
erase = int(layout["flash"]["erase_bytes"])
|
||||||
|
regions = []
|
||||||
|
for part in layout["partitions"]:
|
||||||
|
offset = int(str(part["offset"]), 0)
|
||||||
|
size = int(str(part["size"]), 0)
|
||||||
|
if offset % erase or size % erase:
|
||||||
|
errors.append("partition not erase-aligned: {}".format(part["name"]))
|
||||||
|
if size <= 0 or offset < 0 or offset + size > total:
|
||||||
|
errors.append("partition out of flash bounds: {}".format(part["name"]))
|
||||||
|
regions.append((offset, offset + size, part["name"]))
|
||||||
|
regions.sort()
|
||||||
|
for left, right in zip(regions, regions[1:]):
|
||||||
|
if left[1] > right[0]:
|
||||||
|
errors.append("overlapping partitions: {} and {}".format(left[2], right[2]))
|
||||||
|
image = layout["image"]
|
||||||
|
actual = os.path.getsize(os.path.join(delivery, image["file"]))
|
||||||
|
declared = int(image["bytes"])
|
||||||
|
if actual != declared:
|
||||||
|
errors.append("image byte count differs: actual {} declared {}".format(actual, declared))
|
||||||
|
image_offset = int(str(image["flash_offset"]), 0)
|
||||||
|
erase_length = int(str(image["erase_length"]), 0)
|
||||||
|
if image_offset % erase or erase_length % erase or erase_length < actual:
|
||||||
|
errors.append("image erase range is invalid or unaligned")
|
||||||
|
if not [r for r in regions if r[0] <= image_offset and image_offset + erase_length <= r[1]]:
|
||||||
|
errors.append("image erase range is not contained in a partition")
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append("flash layout validation failed: {}".format(exc))
|
||||||
|
|
||||||
|
|
||||||
|
def check(args):
|
||||||
|
delivery = os.path.abspath(args.delivery)
|
||||||
|
errors = []
|
||||||
|
for name in REQUIRED_ARTIFACTS:
|
||||||
|
if not os.path.isfile(os.path.join(delivery, name)):
|
||||||
|
errors.append("missing artifact: {}".format(name))
|
||||||
|
if errors:
|
||||||
|
print(json.dumps({"reusable": False, "errors": errors}, indent=2))
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = read_json(args.current)
|
||||||
|
stored = read_json(os.path.join(delivery, "build_inputs.json"))
|
||||||
|
if canonical_fingerprint(current) != current.get("fingerprint_sha256"):
|
||||||
|
errors.append("current input fingerprint is internally inconsistent")
|
||||||
|
if canonical_fingerprint(stored) != stored.get("fingerprint_sha256"):
|
||||||
|
errors.append("stored input fingerprint is internally inconsistent")
|
||||||
|
if current.get("fingerprint_sha256") != stored.get("fingerprint_sha256"):
|
||||||
|
errors.append("build-defining inputs differ")
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append("input fingerprint validation failed: {}".format(exc))
|
||||||
|
|
||||||
|
try:
|
||||||
|
sums = parse_checksums(os.path.join(delivery, "SHA256SUMS"))
|
||||||
|
for name in REQUIRED_ARTIFACTS:
|
||||||
|
if name == "SHA256SUMS":
|
||||||
|
continue
|
||||||
|
expected = sums.get(name)
|
||||||
|
if not expected:
|
||||||
|
errors.append("artifact absent from SHA256SUMS: {}".format(name))
|
||||||
|
elif sha256_file(os.path.join(delivery, name)) != expected:
|
||||||
|
errors.append("checksum mismatch: {}".format(name))
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append("checksum validation failed: {}".format(exc))
|
||||||
|
|
||||||
|
validate_layout(delivery, errors)
|
||||||
|
try:
|
||||||
|
summary = read_json(os.path.join(delivery, "summary.json"))
|
||||||
|
for gate in ("G0", "G1", "G2", "G3"):
|
||||||
|
if summary.get("gates", {}).get(gate, {}).get("status") != "PASS":
|
||||||
|
errors.append("prior build gate is not PASS: {}".format(gate))
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append("summary validation failed: {}".format(exc))
|
||||||
|
|
||||||
|
print(json.dumps({"reusable": not errors, "delivery": delivery, "errors": errors}, indent=2, sort_keys=True))
|
||||||
|
return 0 if not errors else 2
|
||||||
|
|
||||||
|
|
||||||
|
def self_test(_args):
|
||||||
|
root = tempfile.mkdtemp(prefix="qspi-cache-test-")
|
||||||
|
try:
|
||||||
|
profile = os.path.join(root, "board.md")
|
||||||
|
source = os.path.join(root, "system.hdf")
|
||||||
|
current = os.path.join(root, "current.json")
|
||||||
|
delivery = os.path.join(root, "delivery")
|
||||||
|
os.makedirs(delivery)
|
||||||
|
with open(profile, "w") as stream:
|
||||||
|
stream.write("board\n")
|
||||||
|
with open(source, "wb") as stream:
|
||||||
|
stream.write(b"hdf")
|
||||||
|
capture(argparse.Namespace(board_profile=profile, input=["HDF=" + source], value=["PETALINUX_VERSION=2018.1"], output=current))
|
||||||
|
payload = read_json(current)
|
||||||
|
if payload["fingerprint_sha256"] != canonical_fingerprint(payload):
|
||||||
|
raise RuntimeError("fingerprint self-test failed")
|
||||||
|
shutil.copyfile(current, os.path.join(delivery, "build_inputs.json"))
|
||||||
|
with open(os.path.join(delivery, "BOOT.BIN"), "wb") as stream:
|
||||||
|
stream.write(b"boot")
|
||||||
|
with open(os.path.join(delivery, "image.ub.qspi"), "wb") as stream:
|
||||||
|
stream.write(b"image")
|
||||||
|
with open(os.path.join(delivery, "MANIFEST.md"), "w") as stream:
|
||||||
|
stream.write("# test\n")
|
||||||
|
with open(os.path.join(delivery, "flash_layout.json"), "w") as stream:
|
||||||
|
json.dump({
|
||||||
|
"flash": {"total_bytes": 262144, "erase_bytes": 65536},
|
||||||
|
"partitions": [
|
||||||
|
{"name": "boot", "offset": "0x0", "size": "0x10000"},
|
||||||
|
{"name": "kernel", "offset": "0x10000", "size": "0x30000"},
|
||||||
|
],
|
||||||
|
"image": {"file": "image.ub.qspi", "bytes": 5, "flash_offset": "0x10000", "erase_length": "0x10000"},
|
||||||
|
}, stream)
|
||||||
|
with open(os.path.join(delivery, "summary.json"), "w") as stream:
|
||||||
|
json.dump({"gates": dict((g, {"status": "PASS"}) for g in ("G0", "G1", "G2", "G3"))}, stream)
|
||||||
|
checksum_names = [name for name in REQUIRED_ARTIFACTS if name != "SHA256SUMS"]
|
||||||
|
with open(os.path.join(delivery, "SHA256SUMS"), "w") as stream:
|
||||||
|
for name in checksum_names:
|
||||||
|
stream.write("{} {}\n".format(sha256_file(os.path.join(delivery, name)), name))
|
||||||
|
if check(argparse.Namespace(current=current, delivery=delivery)) != 0:
|
||||||
|
raise RuntimeError("valid delivery self-test failed")
|
||||||
|
with open(os.path.join(delivery, "image.ub.qspi"), "ab") as stream:
|
||||||
|
stream.write(b"corrupt")
|
||||||
|
if check(argparse.Namespace(current=current, delivery=delivery)) == 0:
|
||||||
|
raise RuntimeError("corrupt delivery self-test failed")
|
||||||
|
print(json.dumps({"self_test": "PASS"}))
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(root, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub = parser.add_subparsers(dest="command")
|
||||||
|
capture_parser = sub.add_parser("capture", help="capture build-defining inputs")
|
||||||
|
capture_parser.add_argument("--board-profile", required=True)
|
||||||
|
capture_parser.add_argument("--input", action="append", default=[])
|
||||||
|
capture_parser.add_argument("--value", action="append", default=[])
|
||||||
|
capture_parser.add_argument("--output", required=True)
|
||||||
|
capture_parser.set_defaults(func=capture)
|
||||||
|
check_parser = sub.add_parser("check", help="check whether a delivery is reusable")
|
||||||
|
check_parser.add_argument("--current", required=True)
|
||||||
|
check_parser.add_argument("--delivery", required=True)
|
||||||
|
check_parser.set_defaults(func=check)
|
||||||
|
test_parser = sub.add_parser("self-test", help="run a non-destructive internal test")
|
||||||
|
test_parser.set_defaults(func=self_test)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not hasattr(args, "func"):
|
||||||
|
parser.print_help()
|
||||||
|
return 2
|
||||||
|
try:
|
||||||
|
return args.func(args)
|
||||||
|
except Exception as exc:
|
||||||
|
print(json.dumps({"error": str(exc)}), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user