第一章:Go跨平台交叉编译的核心原理与失效本质
Go 的跨平台交叉编译能力源于其自举编译器设计与静态链接模型。不同于依赖系统 C 运行时(如 glibc)的 C/C++ 程序,Go 默认将运行时、标准库及所有依赖以静态方式打包进二进制文件中,仅需目标平台的内核 ABI 兼容性即可运行——这使得 GOOS=linux GOARCH=arm64 go build 能在 macOS 主机上直接生成 Linux ARM64 可执行文件,无需虚拟机或容器。
编译器如何实现零依赖交叉构建
Go 工具链内置了完整的多平台目标描述(target triples),包括指令集特性、调用约定、结构体对齐规则等元数据。当设置 GOOS 和 GOARCH 环境变量时,go build 会:
- 切换到对应平台的预编译标准库(位于
$GOROOT/pkg/$GOOS_$GOARCH/); - 使用目标平台专用的汇编器与链接器(如
cmd/compile后端生成目标 ISA 指令); - 禁用主机特定功能(如 macOS 的
kqueue、Windows 的IOCP),自动降级为 POSIX 兼容的poll或epoll实现。
交叉编译失效的典型场景
| 失效原因 | 表现示例 | 解决路径 |
|---|---|---|
| CGO 启用且依赖本地库 | CGO_ENABLED=1 时调用 libsqlite3 |
设定 CC_FOR_TARGET 并提供交叉工具链 |
| 系统调用硬编码 | 直接调用 syscall.Syscall 未适配 |
改用 golang.org/x/sys/unix 封装层 |
| 构建标签(build tags)冲突 | //go:build darwin 误排除目标平台 |
显式添加兼容标签,如 //go:build darwin || linux |
验证交叉编译结果是否真正纯净
使用 file 和 ldd(Linux)或 otool -L(macOS)检查依赖:
# 在 macOS 上构建 Linux 二进制后验证
GOOS=linux GOARCH=amd64 go build -o server-linux main.go
file server-linux # 输出应含 "ELF 64-bit LSB executable, x86-64"
# Linux 容器中执行:
docker run --rm -v $(pwd):/work -w /work debian:sid ldd server-linux # 应显示 "not a dynamic executable"
若 ldd 报告动态依赖,说明 CGO 被意外启用或存在隐式 C 调用——此时需强制禁用:CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -ldflags '-s -w' -o app main.go。
第二章:GOOS/GOARCH环境变量的隐式行为解析
2.1 GOOS/GOARCH对标准库构建路径的动态裁剪机制
Go 构建系统在 go build 阶段依据环境变量 GOOS(目标操作系统)与 GOARCH(目标架构)自动筛选标准库源文件,实现零运行时开销的静态裁剪。
裁剪规则示例
- 源文件名含
_linux.go仅在GOOS=linux时参与编译 - 含
_arm64.go且GOARCH=arm64时激活 - 同时满足
*_unix.go+GOOS=freebsd亦被纳入
构建路径选择逻辑
// src/os/exec/exec_unix.go
// +build unix
//
// 此文件在 GOOS=darwin/linux/freebsd 等 unix 类系统下生效
// 但 GOOS=windows 时完全忽略,不解析、不编译、不链接
该 +build 标签由 go tool compile 在扫描阶段预处理,非预处理器宏;标签匹配失败的文件直接从构建图中移除,不生成 AST。
| GOOS | GOARCH | 激活的标准库路径片段 |
|---|---|---|
| windows | amd64 | src/os/exec/exec_windows.go |
| linux | arm64 | src/syscall/syscall_linux_arm64.go |
| darwin | arm64 | src/runtime/mkfastlog_darwin_arm64.s |
graph TD
A[go build -o app] --> B{读取 GOOS/GOARCH}
B --> C[扫描所有 *.go 文件]
C --> D[按 // +build 和 _os_arch.go 后缀匹配]
D --> E[仅保留匹配文件进入编译流水线]
2.2 环境变量与go build -o标志协同失效的实证复现
当 GOOS/GOARCH 环境变量与 -o 标志共存时,Go 构建系统可能忽略交叉编译目标,仅按宿主机平台生成二进制。
失效复现步骤
- 设置
GOOS=linux GOARCH=arm64 - 执行
go build -o ./bin/app ./main.go - 检查输出文件:
file ./bin/app显示仍为x86_64可执行文件
# 错误示范:环境变量被静默忽略
GOOS=linux GOARCH=arm64 go build -o ./bin/app ./main.go
此命令未触发交叉编译——
-o后路径若含目录(如./bin/),而该目录不存在时,Go 1.21+ 会提前失败;但若目录存在,构建流程绕过环境变量校验逻辑,导致平台信息丢失。
关键约束表
| 条件 | 是否触发交叉编译 | 原因 |
|---|---|---|
-o app(无路径) |
✅ 是 | 环境变量正常生效 |
-o ./bin/app(路径存在) |
❌ 否 | 构建器跳过 GOOS/GOARCH 验证阶段 |
-o ./bin/app(路径不存在) |
⚠️ 中断 | 报错 mkdir ./bin: no such file,不生成任何输出 |
graph TD
A[解析 -o 参数] --> B{输出路径含目录?}
B -->|是| C[检查目录是否存在]
C -->|存在| D[跳过平台环境变量校验]
C -->|不存在| E[报错退出]
B -->|否| F[正常读取 GOOS/GOARCH]
2.3 多目标平台下GOOS/GOARCH组合的合法边界验证实验
Go 编译器对 GOOS/GOARCH 组合存在隐式约束,需通过实证验证其合法边界。
实验设计思路
遍历常见平台组合,执行 go tool dist list 并交叉校验构建可行性:
# 获取所有官方支持的 target 列表
go tool dist list | grep -E '^(linux|darwin|windows)/(amd64|arm64|386|arm)' | head -n 6
此命令筛选主流 OS/ARCH 对,避免无效组合(如
windows/arm在 Go 1.21+ 已弃用)。go tool dist list输出由src/cmd/dist/testdata/goos_goarch.txt驱动,反映编译器实际支持能力。
合法性验证结果
| GOOS | GOARCH | 可构建 | 原因说明 |
|---|---|---|---|
| linux | riscv64 | ✅ | 自 Go 1.19 起稳定支持 |
| darwin | 386 | ❌ | macOS 10.15+ 已移除 32 位内核支持 |
| windows | wasm | ✅ | 仅支持 GOOS=js GOARCH=wasm,此处非法 |
构建失败路径分析
graph TD
A[go build -o app -ldflags=-s] --> B{GOOS/GOARCH 是否在<br>internal/buildcfg/known.go 中注册?}
B -->|否| C[“# runtime: unknown architecture”]
B -->|是| D{是否启用 CGO?<br>CGO_ENABLED=0 时禁用部分平台}
D -->|是| E[链接器报错:no such file or directory for libgcc]
核心结论:合法性取决于 buildcfg 注册、运行时支持及工具链完备性三重校验。
2.4 构建缓存(build cache)中GOOS/GOARCH元数据污染的定位与清理
污染根源识别
Go 构建缓存默认不隔离 GOOS/GOARCH 组合,跨平台构建(如 GOOS=linux GOARCH=arm64 go build 后紧接 GOOS=darwin GOARCH=amd64)会复用错误平台的 .a 归档,导致链接失败或运行时 panic。
快速定位命令
# 列出缓存中含目标平台标识的对象
go tool cache -list | grep -E "(linux|darwin).*arm64|amd64" | head -5
该命令调用 Go 内置缓存工具枚举所有缓存键,正则匹配平台标识;-list 输出格式为 key size timestamp,便于人工比对时间戳与构建上下文。
清理策略对比
| 方法 | 范围 | 安全性 | 执行开销 |
|---|---|---|---|
go clean -cache |
全局清空 | ⚠️ 高(丢失所有缓存) | 高 |
go tool cache -remove <key> |
精确键删除 | ✅ 推荐 | 低 |
GOCACHE=/tmp/go-cache-$GOOS-$GOARCH go build |
隔离路径 | ✅ 最佳实践 | 中(需环境切换) |
自动化修复流程
graph TD
A[检测构建失败日志] --> B{是否含“incompatible cpu”?}
B -->|是| C[提取GOOS/GOARCH环境变量]
C --> D[生成缓存键哈希前缀]
D --> E[执行go tool cache -remove]
2.5 跨shell会话与Docker构建上下文中GOOS/GOARCH继承性陷阱
Go 构建环境变量 GOOS 和 GOARCH 在跨 shell 会话或 Docker 构建中极易被隐式继承,导致意外的交叉编译行为。
环境变量泄漏场景
- 本地终端设置
GOOS=windows后启动docker build .bashrc中全局导出GOARCH=arm64,被RUN go build捕获- 多阶段构建中
build-stage的环境未显式重置,污染final-stage
构建命令对比表
| 场景 | 命令 | 实际目标平台 | 风险 |
|---|---|---|---|
| 未清理环境 | docker build . |
继承宿主 GOOS/GOARCH |
构建出错或镜像不兼容 |
| 显式覆盖 | docker build --build-arg GOOS=linux . |
linux/amd64(默认) |
安全可控 |
| 构建阶段内覆盖 | RUN GOOS=linux GOARCH=arm64 go build -o app . |
强制指定 | 推荐实践 |
# Dockerfile 片段:显式隔离构建环境
FROM golang:1.22-alpine AS builder
# 关键:清空可能继承的 GOOS/GOARCH
ENV GOOS=linux GOARCH=amd64
RUN go build -o /app .
FROM alpine:latest
COPY --from=builder /app .
该
ENV行强制重置构建环境,避免父 shell 或构建缓存中残留的GOOS/GOARCH干扰。Docker 构建器在解析RUN前会合并ENV,确保go build始终在预期目标平台上执行。
graph TD
A[宿主 Shell] -->|export GOOS=windows| B[Docker Build]
B --> C{构建阶段 ENV 是否显式覆盖?}
C -->|否| D[继承宿主值 → 错误二进制]
C -->|是| E[使用 ENV 值 → 正确目标平台]
第三章:cgo交叉链接器链的关键依赖剖析
3.1 CGO_ENABLED=0与CGO_ENABLED=1在交叉编译中的ABI断裂点实测
当交叉编译 Go 程序至 linux/arm64 时,CGO 启用状态直接决定底层调用约定与符号可见性:
# 禁用 CGO:纯 Go 运行时,无 libc 依赖
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o app-static main.go
# 启用 CGO:链接 musl/glibc,引入动态 ABI 约束
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -o app-dynamic main.go
逻辑分析:
CGO_ENABLED=0强制使用 Go 自实现的系统调用(如syscall.Syscall封装),规避 C ABI;而CGO_ENABLED=1触发cgo代码生成,导致_cgo_export.h中函数签名与目标平台 C 工具链 ABI 严格对齐——若交叉工具链版本不匹配(如 glibc 2.31 vs 2.35),struct stat字段偏移差异即引发运行时 panic。
关键 ABI 断裂点对比
| 维度 | CGO_ENABLED=0 | CGO_ENABLED=1 |
|---|---|---|
| 运行时依赖 | 静态链接,无 libc | 动态链接,强依赖目标平台 libc 版本 |
time.Now() 实现 |
vdso 直接读取 TSC |
调用 clock_gettime(2) libc wrapper |
| 符号导出 | 仅导出 Go 符号(//export 失效) |
支持 //export,生成 C ABI 兼容符号 |
实测崩溃场景流程
graph TD
A[go build with CGO_ENABLED=1] --> B[生成 _cgo_main.o]
B --> C[调用 aarch64-linux-gnu-gcc 链接]
C --> D{libc symbol resolution}
D -->|版本不匹配| E[undefined reference to 'getrandom']
D -->|内核不支持| F[syscall fallback missing → SIGILL]
3.2 交叉工具链(如x86_64-w64-mingw32-gcc)与pkg-config路径绑定实践
交叉编译时,pkg-config 默认查找本地系统库路径,导致 x86_64-w64-mingw32-gcc 无法定位 MinGW 目标平台的 .pc 文件。
正确绑定方式
设置环境变量强制指定目标平台 pkg-config 路径:
export PKG_CONFIG_PATH="/usr/x86_64-w64-mingw32/lib/pkgconfig"
export PKG_CONFIG_SYSROOT_DIR="/usr/x86_64-w64-mingw32"
export PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
PKG_CONFIG_PATH:指向交叉工具链安装的.pc文件目录;PKG_CONFIG_SYSROOT_DIR:使 pkg-config 自动裁剪头文件/库路径前缀;PKG_CONFIG_ALLOW_SYSTEM_CFLAGS:允许保留-I/-L中的绝对路径(需谨慎)。
典型错误对照表
| 场景 | 表现 | 原因 |
|---|---|---|
未设 PKG_CONFIG_SYSROOT_DIR |
-I/usr/include 被误用 |
头文件路径未重映射到 sysroot |
混用 --host 与本地 pkg-config |
找到 glib-2.0.pc 但链接失败 |
库 ABI 不匹配(Linux GLib vs MinGW GLib) |
graph TD
A[configure.ac] --> B[AC_CHECK_PROG(pkg-config)]
B --> C{PKG_CONFIG_PATH set?}
C -->|Yes| D[解析 x86_64-w64-mingw32/*.pc]
C -->|No| E[回退至 /usr/lib/pkgconfig → 链接失败]
3.3 C头文件搜索路径(-I)、库路径(-L)及符号解析顺序的调试技巧
头文件与库路径的优先级本质
GCC 按固定顺序解析 -I 和 -L:命令行指定路径 > 环境变量(CPATH/LIBRARY_PATH)> 系统默认路径(如 /usr/include, /usr/lib/x86_64-linux-gnu)。
快速验证路径是否生效
# 查看预处理器实际搜索路径(含-I效果)
gcc -E -v -x c /dev/null 2>&1 | sed -n '/#include <...>/,/^$/p'
# 查看链接器库搜索路径(含-L效果)
gcc -Wl,--verbose 2>/dev/null | grep "SEARCH_DIR"
-E -v 触发预处理并输出详细路径;-Wl,--verbose 将链接器详细日志转出,SEARCH_DIR 行即为 -L 生效顺序。
符号未定义?检查链接时序
| 阶段 | 关键约束 |
|---|---|
| 编译(-I) | 影响 #include 解析,不决定符号可见性 |
| 链接(-L/-l) | 库必须在目标文件之后指定,否则符号无法解析 |
graph TD
A[源码 #include <foo.h>] --> B[预处理:按-I顺序查找foo.h]
B --> C[编译生成.o:仅记录未定义符号如 foo_init]
C --> D[链接:-L/path -lfoo → 按顺序搜索libfoo.so/a]
D --> E[符号解析:从左到右扫描库,首次匹配即停]
第四章:11个隐式依赖陷阱的逐项攻防演练
4.1 系统级pkg-config版本与target平台架构不匹配的检测与降级方案
当交叉编译嵌入式项目时,宿主机 pkg-config 常因 ABI/架构差异(如 x86_64 宿主调用 aarch64-target 库)返回错误路径或缺失 --host 标识。
检测逻辑
通过环境变量与探针命令联合验证:
# 检查 pkg-config 是否支持 target 架构标识
if ! $PKG_CONFIG --variable=host_system 2>/dev/null | grep -q "aarch64\|armv7"; then
echo "WARN: pkg-config lacks target-awareness" >&2
fi
该脚本利用 --variable=host_system(需 pkg-config ≥ 0.29)探测其内置目标系统元数据;若失败,则表明为旧版通用 pkg-config,无法区分 target。
降级策略优先级
- ✅ 优先启用
PKG_CONFIG_PATH指向 target sysroot 下的pkgconfig/ - ⚠️ 次选:设置
PKG_CONFIG_SYSROOT_DIR+--define-variable=exec_prefix=/usr - ❌ 禁止直接覆盖
PATH切换二进制——易引发工具链污染
| 方案 | 可靠性 | 需求条件 |
|---|---|---|
PKG_CONFIG_PATH 覆盖 |
高 | target pkgconfig 文件存在且完整 |
--define-variable 注入 |
中 | 依赖库 .pc 文件支持变量展开 |
graph TD
A[执行 pkg-config --modversion foo] --> B{返回路径含 /usr/lib ?}
B -->|是| C[触发架构不匹配告警]
B -->|否| D[视为 target-aware]
4.2 libc变体(glibc/musl/uClibc)导致的运行时panic复现与静态链接修复
不同 C 标准库实现对符号解析、线程模型和系统调用封装存在本质差异,常引发 SIGSEGV 或 abort() 等运行时 panic。
复现场景示例
以下程序在 Alpine Linux(musl)中正常,但在 CentOS(glibc)容器内因 getaddrinfo 符号绑定冲突 panic:
// main.c
#include <netdb.h>
int main() {
struct addrinfo *res;
return getaddrinfo("localhost", "80", NULL, &res); // musl 返回 0;glibc 在某些 LD_PRELOAD 环境下可能重入失败
}
逻辑分析:
getaddrinfo在 glibc 中依赖 NSS 模块动态加载,而 musl 内联实现。若镜像混用(如基于 glibc 编译但运行于 musl),或交叉链接未指定-static,将触发 PLT 解析失败。
静态链接修复方案
- 使用
gcc -static -musl-gcc(musl)或gcc -static(glibc,需安装glibc-static) - 推荐统一采用 musl 工具链构建最小化二进制
| libc | 动态体积 | 线程模型 | 兼容性风险 |
|---|---|---|---|
| glibc | ~2.3 MB | NPTL | 高(NSS/NSCD 依赖) |
| musl | ~0.5 MB | Lightweight | 低(无运行时插件) |
| uClibc | ~0.3 MB | 可裁剪 | 极高(已弃用) |
graph TD
A[源码] --> B{链接方式}
B -->|动态链接| C[glibc/musl 运行时绑定]
B -->|静态链接| D[符号全嵌入二进制]
C --> E[跨环境 panic 风险↑]
D --> F[镜像体积↑ 但兼容性↑]
4.3 交叉编译中net.LookupHost等标准库函数的DNS resolver隐式依赖剥离
Go 标准库中 net.LookupHost 等函数在交叉编译时会隐式链接宿主机的 libc DNS resolver(如 glibc 的 getaddrinfo),导致目标平台(如 musl 或无 libc 环境)运行失败。
静态链接与 DNS 模式切换
Go 提供 netgo 构建标签强制使用纯 Go DNS 解析器:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags netgo -o app .
CGO_ENABLED=0:禁用 cgo,彻底剥离 libc 依赖-tags netgo:优先启用net包内置的 DNS 实现(基于 UDP +/etc/resolv.conf)
DNS 解析路径对比
| 场景 | 解析器来源 | 依赖 libc | 支持自定义 nameserver |
|---|---|---|---|
| 默认(cgo 启用) | libc getaddrinfo |
✅ | ❌(受系统配置限制) |
netgo 模式 |
net/dnsclient.go |
❌ | ✅(读取 /etc/resolv.conf) |
剥离验证流程
graph TD
A[源码调用 net.LookupHost] --> B{CGO_ENABLED=0?}
B -->|是| C[自动选用 netgo resolver]
B -->|否| D[链接 libc getaddrinfo]
C --> E[解析逻辑完全托管于 Go 运行时]
4.4 CGO_CFLAGS/CXXFLAGS中-fPIC、-target等标志与目标平台ISA的兼容性验证
CGO交叉编译时,CGO_CFLAGS 和 CGO_CXXFLAGS 中的编译器标志必须与目标平台的指令集架构(ISA)严格对齐,否则将导致链接失败或运行时非法指令异常。
关键标志语义解析
-fPIC:生成位置无关代码,必需用于构建共享库(如 Go 的 cgo 插件);-target:显式指定目标三元组(如aarch64-unknown-linux-gnu),覆盖主机默认,决定 ISA、ABI 和内置宏定义。
兼容性验证示例
# 验证目标平台是否支持 ARM64 SIMD 指令(如 NEON)
CGO_CFLAGS="-fPIC -target aarch64-unknown-linux-gnu -march=armv8.2-a+simd" \
go build -buildmode=c-shared -o libmath.so math.go
逻辑分析:
-march=armv8.2-a+simd显式启用 ARMv8.2 及 NEON 扩展;若目标 CPU 仅支持armv8.0-a,链接器虽不报错,但运行时执行FMLA等指令会触发SIGILL。-target确保 clang 正确设置__aarch64__和__ARM_FEATURE_SIMD宏,影响头文件条件编译路径。
常见 ISA-Flag 匹配表
| 目标平台 | 推荐 -target |
必需 -march |
关键依赖特性 |
|---|---|---|---|
| x86_64 | x86_64-pc-linux-gnu |
x86-64-v3 |
AVX2, BMI2 |
| arm64 | aarch64-unknown-linux-gnu |
armv8.4-a+crypto |
AES, SHA, PMULL |
验证流程(mermaid)
graph TD
A[设定 CGO_CFLAGS] --> B{target 与 host ISA 是否匹配?}
B -->|否| C[编译失败:unknown target]
B -->|是| D{march 特性是否被目标内核/硬件支持?}
D -->|否| E[运行时 SIGILL]
D -->|是| F[通过]
第五章:构建可复现、可审计、可分发的跨平台Go制品
Go 语言的交叉编译能力与静态链接特性,为构建跨平台制品提供了天然优势。但仅靠 GOOS=linux GOARCH=arm64 go build 远不足以满足生产级发布要求——缺乏确定性构建环境、缺失二进制溯源信息、缺少签名验证机制,将直接导致审计失败与供应链风险。
构建环境标准化:Docker + BuildKit 多阶段锁定
使用 docker buildx build 配合自定义构建器实例,强制所有构建在完全隔离的 Debian 12 + Go 1.22.5 容器中执行。以下 Dockerfile 片段确保 Go 工具链版本、glibc 替代品(musl)、CA 证书均来自固定 SHA256 哈希镜像:
FROM --platform=linux/amd64 golang:1.22.5-alpine3.19 AS builder
RUN apk add --no-cache ca-certificates git && update-ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -buildid=" -o bin/app-linux-amd64 ./cmd/app
可复现性保障:go.sum 锁定 + 构建参数固化
Go 模块校验和 go.sum 必须随源码一同提交,并在 CI 中启用 GOFLAGS=-mod=readonly。同时,所有构建命令显式指定 -trimpath(消除绝对路径)、-buildid=(禁用随机 build ID)、-ldflags="-s -w"(剥离调试符号与 DWARF),确保相同输入产生字节级一致的二进制:
| 参数 | 作用 | 是否必需 |
|---|---|---|
-trimpath |
移除源码绝对路径,避免影响 build ID | ✅ |
-buildid= |
清空 build ID 字段,防止哈希漂移 | ✅ |
-ldflags="-s -w" |
剥离符号表与调试信息,减小体积并提升一致性 | ✅ |
制品元数据注入:git commit + semver + SBOM 生成
构建时通过 go build -ldflags "-X main.commit=$(git rev-parse HEAD) -X main.version=v1.8.3" 将 Git 提交哈希与语义化版本注入二进制。同时,使用 syft 生成 SPDX 2.3 格式软件物料清单(SBOM):
syft ./bin/app-linux-amd64 -o spdx-json=sbom-spdx.json
该 SBOM 文件随制品一同归档至 Nexus Repository Manager,供安全团队扫描已知漏洞(如 CVE-2023-45857)及许可证合规审查。
跨平台制品矩阵自动化构建
借助 GitHub Actions 的 matrix 策略,一次性生成 6 种目标平台组合:
strategy:
matrix:
os: [ubuntu-latest, macos-14, windows-2022]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
每个作业输出带平台后缀的二进制(如 app-darwin-arm64)、对应 checksums.txt(含 SHA256/SHA512)、签名文件(.asc 使用 GPG 离线密钥签署)及 SBOM 副本,全部打包为 app-v1.8.3-artifacts.zip。
审计追踪闭环:从制品到源码的单向可追溯链
每个发布的 .zip 包内含 provenance.intoto.jsonl(in-toto v1.0 格式),记录构建环境指纹(Docker image digest)、输入 commit、签名者公钥 ID、完整构建命令与输出哈希。该文件经 Sigstore Fulcio 签名并上传至 Rekor 透明日志,任何第三方均可独立验证该制品是否由指定仓库特定 commit 构建而成。
flowchart LR
A[Git Commit a1b2c3] --> B[Build Job in GitHub Actions]
B --> C[Binary app-linux-amd64]
B --> D[SBOM sbom-spdx.json]
B --> E[Provenance provenance.intoto.jsonl]
C --> F[SHA256 Hash in checksums.txt]
D --> F
E --> F
F --> G[Rekor Transparency Log Entry] 