负载和清单在有限保留窗口内,从明确的生产者交给执行验证的消费方。字节一致或文件名熟悉,本身不能证明来源或永久可用。

CI 产物需要明确的交接契约

CI 的上游任务可以成功结束,下游却拿到错误文件。产物是某个步骤生成并保存下来、交给其他任务、人员或发布流程使用的输出,仅靠名字通常不足以识别它。应把交接视为一份契约,说明生产它的运行、源码版本、负载内容、完整性证据、访问策略和可用时间窗口。这样,“下载构建结果”才成为能够核对的操作,而不是在共享目录里猜测哪个文件最新。 明确的交接核对预期生产者、约定文件与负载完整性,并规划保留期和访问。保留的通过记录只覆盖本地归档,未验证托管过期或来源真实性。本次绘图未重跑示例。 分清传递目的与保存期限 产物可以是二进制、测试报告、覆盖率数据,也可以是之后进入发布流程的软件包;不能断言它天然不属于发布输入。依赖缓存的目的则是加速可以重建的工作,二者不应混为一谈。选择存储和保留期之前,先确定审批、诊断和回滚需要哪些文件、需要多久。过期以后重新构建,可能生成不同字节,也需要重新验证,不能把重建视为取回原产物。 GitHub Actions 允许自定义产物保留时间,但受到仓库、组织或企业上限约束。安排流程时,要按真实配置考虑周末和延迟审批;请求一个较长保留值,并不能突破平台上限。确实需要长期留存的发布输入,应转入项目使用的持久发布存储或对象存储,并明确生命周期。短期任务存储即使今天能下载,也不应被说明成无限期的发布归档。 坏掉的样子 随手下载“最近一次成功运行”,可能拿到另一个提交、分支或工作流的文件。应明确解析目标生产任务,并核对它的源码与当前批准发布的变化之间的关系。下游还需要等待所需文件真正产生;空上传、缺失产物不能悄悄变成部署工作区里碰巧残留的旧文件。任务变绿与正确负载已经交付,是需要分别检查的事实。 完整性与来源真实性回答不同问题。校验和可以发现负载变化,但能同时替换文件和校验和的人,也能造出自洽的一对文件。来源声明还需要验证它指向的产物、签名方或工作流身份,以及项目要求的策略。二进制旁边存在一份证明文件,并不代表消费方已经执行了验证。必须看真实的验证结果,而不是只看相关配置或附件存在。 谁能访问产物,取决于平台、仓库可见性与授予的权限,不能一概等同于“能看到工作流的人”。即便如此,也只应上传交接必需的文件。凭据、环境变量全集、私有请求头和未经筛选的日志,一旦打包就可能留下额外副本。保存之前要检查成员列表及其内容,尤其当构建输入来自尚未信任的贡献时,不能只靠压缩包名称判断它是否适合交给下游。 快速验证 保存脚本为 artifacts.py 并执行 python3 artifacts.py,只需 Python 标准库。生产方在临时目录创建 ZIP,包含 app.txt 与清单;清单记录本地运行编号、示意性的源码摘要和负载摘要。消费方只读取约定成员,先比较预期生产者,再返回实际负载。这里没有解压任意归档成员,也没有连接真实 CI 平台。 from hashlib import sha256 from pathlib import Path from tempfile import TemporaryDirectory from zipfile import ZipFile import json payload = b'local build output\n' source_digest = sha256(b'example source revision\n').hexdigest() manifest = {'producer_run': 'local-run-41', 'source_sha256': source_digest, 'files': {'app.txt': sha256(payload).hexdigest()}} def produce(path, data): with ZipFile(path, 'w') as archive: archive.writestr('manifest.json', json.dumps(manifest, sort_keys=True)) archive.writestr('app.txt', data) def consume(path, expected_run): with ZipFile(path) as archive: if sorted(archive.namelist()) != ['app.txt', 'manifest.json']: raise ValueError('unexpected member set') received = json.loads(archive.read('manifest.json')) if (received['producer_run'] != expected_run or received['source_sha256'] != source_digest): raise ValueError('producer identity mismatch') data = archive.read('app.txt') if sha256(data).hexdigest() != received['files']['app.txt']: raise ValueError('payload digest mismatch') return data with TemporaryDirectory() as directory: archive = Path(directory) / 'handoff.zip' produce(archive, payload) assert consume(archive, 'local-run-41') == payload rejected = [] try: consume(archive, 'local-run-42') except ValueError as error: rejected.append(str(error)) produce(archive, payload + b'changed\n') try: consume(archive, 'local-run-41') except ValueError as error: rejected.append(str(error)) archive.unlink() try: consume(archive, 'local-run-41') except FileNotFoundError: rejected.append('artifact missing') assert rejected == ['producer identity mismatch', 'payload digest mismatch', 'artifact missing'] print(json.dumps({'valid_payload_bytes': len(payload), 'rejected': rejected, 'platform_retention_tested': False})) 正常交接返回十九字节。用 local-run-42 去领取实际来自 local-run-41 的归档,会得到生产者身份不匹配;保留旧清单、替换文件内容,会得到摘要不匹配;删除归档后继续下载,则得到文件缺失。最终结果必须列出这三种拒绝情况,任何失败都不会转而消费另一个构建。这样可以看清下游契约的失败分支确实执行了。 ...

九月 3, 2027 · 1 分钟 · XB Field Notes
在 pip 之前复制 app.py,会让仅源码变化也触发安装重跑。先复制依赖输入、安装,再复制 app.py,可复用该安装结果。

先复制依赖输入,再复制应用代码

Dockerfile 顺序决定昂贵构建操作依赖哪些先前输入。如果应用源码在依赖安装之前被复制,即使依赖声明未变,修改源码也可能让安装重跑。先复制依赖输入、安装依赖,再复制应用代码,可以建立更小的依赖边界,让它与安装器真正需要的工作匹配。这里优化的是依赖关系,而不只是把几行命令挪到看起来更整齐的位置。 依据原文历史样例说明机制。当前审核仍为已修订、执行受阻。本次绘图未执行 Docker 命令,未增加运行、页面、集成或发布验证。 坏掉的样子 在软件包安装前使用宽泛的 COPY . .,会让文档、生成输出或无关源码编辑,都可能成为重做昂贵工作的原因。问题不在于 COPY 天生很慢,而在于后续安装依赖了一份包含无关变化的文件系统状态。这些变化并不是选择和安装依赖所必需,却通过前置步骤扩大了失效范围。 如果安装还读取锁文件、本地包源码、补丁、工作区元数据或包管理器配置,仅复制一个清单也不够。遗漏真实输入,可能保留已经不符合构建意图的缓存结果。正确边界必须包含安装器实际使用的全部输入,而不是只选择常见 Dockerfile 示例里出现的某个文件名。否则,表面命中率提高,实际正确性却下降。 缓存顺序也不能修复可变依赖。相同安装命令可能一直保留旧结果,而干净构建却从远端解析到不同内容。固定版本和校验哈希解决产物选择问题,控制 COPY 顺序解决复用问题。两者都需要处理,构建更快并不能证明软件包图可复现,也不能证明依赖已经更新到本次发布要求的版本。 快速验证 使用宿主 Python、Docker 构建器和已取得的固定基础镜像运行脚本。它先通过 HTTPS 下载两个公开的 idna wheel,并在构建前验证固定 SHA-256。构建 RUN 指令关闭外部网络,pip 从本地 wheel 目录安装并强制要求哈希;镜像解析仍可能访问仓库。实测环境为 Docker 29.3.1,Linux arm64 基础镜像内 Python 为 3.11.16: ...

八月 17, 2027 · 2 分钟 · XB Field Notes
历史双目录示例中,只做编译器路径映射会使目标文件一致,但最终可执行文件仍不同。目标文件相同不能证明发布二进制相同。

可复现构建把源码与产物对应起来

可复现构建的含义是:另一方依据声明的源码、构建步骤和相关环境,能够逐字节重建指定产物。这个承诺必须说明比较哪些输出;如果交付的可执行文件不同,只比较目标文件相同是不够的。复现结果为源码与分发内容之间的联系提供证据,而源码审阅、来源证明和签名分别回答其他问题。两次相同的恶意构建仍然有害,被污染的工具链也可能稳定复现自己的异常行为。 图示为文章记录的 Apple 工具链比较,包含仅做编译器处理时最终可执行文件仍不一致的情况。平台专用重跑仍受阻,本次绘图也未重跑构建。本地一致不等于独立复现,也不证明源码安全。 记录真正影响字节的输入 源码版本与编译器版本只是输入的一部分。依赖、目标架构、SDK、链接器、参数、文件遍历顺序、目录路径、时间戳、区域设置和打包工具,都可能进入最终结果。应固定实际相关的输入,并说明比较边界。容器镜像有助于描述环境,却不会自动消除网络下载、宿主机挂载或变化的时间戳。一个镜像标签也不必然代表永远不变的镜像内容。 时间宏和调试信息是常见差异来源。SOURCE_DATE_EPOCH 为支持该约定的工具提供稳定时间,并不会改写所有程序对当前时间的理解。编译器的前缀映射能够规范化嵌入的源文件路径;链接器还可能独立记录目标文件位置或其他元数据。因此,编译阶段消除了差异,仍应继续检查实际链接输出,不能在中间产物相同时就停止验证。 坏掉的样子 只比较挑选出的某个段,可能掩盖真正发布文件中的不同。应明确可复现性覆盖可执行文件、调试符号、压缩归档还是带签名安装包。如果签名过程加入会变化的数据,就说明并论证可复现负载的边界,同时单独校验签名后的分发包。还应记录 SDK 和链接器的构建标识;仅有编译器版本,无法重建另外两者的输入。不能悄悄删除难以处理的字节,再宣称原始文件完全相等;比较范围本身也是需要审阅的契约。 同一台机器上的两次摘要一致,是这些输入条件下本地重复构建的证据,不等于多个独立构建方已经验证,也不能证明源码与工具正确。反过来,摘要不一致首先意味着需要调查,不能立即断定发生了攻击。应定位差异来自哪个组件,再判断是不是有输入没有纳入环境声明。没有记录 SDK、链接参数或打包规则,可能就足以让相同源码生成不同文件。 快速验证 此前记录的实验使用 arm64 macOS、Apple clang 21.0.0 及其本机链接器;本轮没有重新执行这个平台专用构建。保存为 repro-build.py,安装命令行开发工具后运行 python3 repro-build.py。链接参数属于 Apple 工具链,不应假设 Linux 链接器也接受。脚本创建两个临时构建目录,用三种处理方式编译同一小段 C 程序;目录名差异是刻意保留的输入扰动。 from hashlib import sha256 from pathlib import Path from tempfile import TemporaryDirectory import json import os import subprocess source = '#include <stdio.h>\nint main(void) { puts(__FILE__); puts(__DATE__ " " __TIME__); }\n' with TemporaryDirectory() as directory: base = Path(directory) hashes = {mode: {'objects': [], 'executables': []} for mode in ('unmapped', 'compiler-only', 'complete')} output = [] for name in ('one', 'two'): root = base / name root.mkdir() src = root / 'main.c' src.write_text(source) env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C', 'SOURCE_DATE_EPOCH': '1700000000'} for mode in hashes: obj = root / (mode + '.o') binary = root / mode flags = [] if mode == 'unmapped' else [f'-ffile-prefix-map={root}=/src'] subprocess.run(['clang', '-g', '-O0', *flags, '-c', str(src), '-o', str(obj)], cwd=root, env=env, check=True, capture_output=True) os.utime(obj, (1700000000, 1700000000)) linker = ['-Wl,-reproducible'] if mode == 'complete': linker.append(f'-Wl,-oso_prefix,{root}/') subprocess.run(['clang', *linker, str(obj), '-o', str(binary)], cwd=root, env=env, check=True, capture_output=True) hashes[mode]['objects'].append(sha256(obj.read_bytes()).hexdigest()) hashes[mode]['executables'].append(sha256(binary.read_bytes()).hexdigest()) if mode == 'complete': output.append(subprocess.check_output([str(binary)], text=True).splitlines()) equality = {mode: {kind: pair[0] == pair[1] for kind, pair in records.items()} for mode, records in hashes.items()} assert equality == {'unmapped': {'objects': False, 'executables': False}, 'compiler-only': {'objects': True, 'executables': False}, 'complete': {'objects': True, 'executables': True}} assert output == [['/src/main.c', 'Nov 14 2023 22:13:20']] * 2 print(json.dumps({'sha256': hashes, 'equal_between_directories': equality, 'complete_program_output': output, 'same_host_only': True})) 所有处理都使用时间值 1700000000,并固定目标文件的修改时间。没有路径映射时,目标文件和可执行文件都不同。只做编译器映射后,目标文件摘要已经相等,最终可执行文件仍然不同:链接器调试映射中还保留着各自的目标文件路径。这个中间失败说明,编译阶段通过的检查不能直接为实际发布的二进制文件背书。 ...

八月 15, 2027 · 2 分钟 · XB Field Notes
两个不同应用输出共享同一固定基础,所选构建参数不同;只固定 FROM 并不能确定完整发布。

固定基础镜像只固定一个输入,完整发布还需要其他记录

按摘要固定容器基础镜像,可以选择确定的镜像对象,避免依赖会移动的标签。这消除了一类意外构建变化,但不会让所有派生镜像或运行实例都完全相同。应用源码、构建参数、下载依赖、目标平台、运行配置和持久数据,仍可能改变构建结果及其行为。因此,基础镜像不可变是一个明确的输入约束,不是完整发布已经可复现的证明。 坏掉的样子 两个 Dockerfile 可以使用相同 FROM 摘要,却生成不同应用。即使 Dockerfile 一样,构建参数变化或未固定的下载取得新内容,也可能改变输出。所以,仅声明基础镜像不可变,无法证明完整构建是确定性的。决定这个性质的是全部输入,以及施加在输入上的转换过程;遗漏任何真正影响结果的部分,都会留下解释缺口。 完全相同的应用镜像,在不同环境变量下运行,也可能选择不同端点、功能行为或存储。可写绑定挂载还能替换镜像内已经测试过的文件。把配置留在镜像外通常有价值,但只要配置参与定义发布行为,它仍然需要版本管理和审阅。外置不等于无须控制,更不意味着配置变化不会影响同一镜像的实际表现。 普通容器还拥有可写文件系统层。紧急编辑可以改变一个实例,而镜像和其他实例保持原样。之后从未修改的镜像替换实例,编辑就会消失。因此,不可变部署实践需要把产物替换与受控运行状态结合起来;只在 FROM 后写摘要,无法自动强制团队遵守这种操作方式,也无法阻止容器内手工修补。 固定基础只控制一个输入。只读根约束特定写入,而挂载、数据与设置仍相互独立。基础更新需要主动审阅、重建和测试。Docker 示例执行仍受阻,本次未重跑。 快速验证 使用宿主 Python、Docker 构建器和已取得的固定基础镜像运行脚本。此前记录的实测环境为 Docker 29.3.1、Linux arm64,镜像内 Python 3.11.16;本轮没有重跑 Docker。容器网络与构建 RUN 网络被关闭,但镜像解析仍可能访问仓库;构建步骤没有下载软件包。实验结束删除自己的临时容器和标签,不修改任何既有服务,以便把观察限定在明确的受控输入上: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def inspect(image):return json.loads(docker('image','inspect',image).stdout)[0] name='xbcatm-pinning-'+uuid.uuid4().hex[:10];tags=[name+':one',name+':two'];container=name+'-running' try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory) (root/'Dockerfile').write_text(f'FROM {BASE}\nARG MESSAGE\nRUN printf "%s" "$MESSAGE" > /built-message\nENV MODE=default\nCOPY app.py /app.py\nCMD ["python", "/app.py"]\n') (root/'app.py').write_text('from pathlib import Path\nimport os\nprint(Path("/built-message").read_text(),os.environ["MODE"])\n') layers=inspect(BASE)['RootFS']['Layers'] for tag,message in zip(tags,['one','two']): docker('build','--network','none','--build-arg','MESSAGE='+message,'-t',tag,str(root)) assert inspect(tag)['RootFS']['Layers'][:len(layers)]==layers assert inspect(tags[0])['Id']!=inspect(tags[1])['Id'] assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='one default' assert docker('run','--rm','--network','none',tags[1]).stdout.strip()=='two default' assert docker('run','--rm','--network','none','-e','MODE=changed',tags[0]).stdout.strip()=='one changed' print('both builds use the same pinned base layers; different build arguments produce different artifacts') print('the identical application image behaves differently when runtime configuration changes') docker('run','-d','--name',container,'--network','none',tags[0],'python','-c','import time;time.sleep(60)') docker('exec',container,'python','-c','from pathlib import Path;Path("/built-message").write_text("edited")') assert docker('exec',container,'python','/app.py').stdout.strip()=='edited default' assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='one default' failed=docker('run','--rm','--network','none','--read-only',tags[0],'python','-c','from pathlib import Path;Path("/built-message").write_text("edited")',check=False) assert failed.returncode!=0 and 'Read-only file system' in failed.stderr print('a writable container can drift while its image stays unchanged; a read-only root rejects that particular write') finally: docker('rm','-f',container,check=False);docker('image','rm',*tags,check=False) 两次构建使用相同基础引用和应用源码,选择的构建差异只有 MESSAGE;Dockerfile 把它写入 /built-message。第一份镜像输出 one default,第二份输出 two default。完整镜像身份不同,但检查确认它们有序的基础文件系统层前缀完全一致。这里同时对照了公共输入和最终行为,避免把基础相同误当成全部相同。 ...

四月 30, 2027 · 2 分钟 · XB Field Notes
两个普通容器引用同一不可变镜像,但各有独立可写层。只修改第一个容器,不会改变兄弟容器或原镜像。

相关镜像共享文件层,运行中的容器各自保存修改

镜像用有序的不可变层变更集描述文件系统,并带有默认命令等配置。相关镜像可以引用相同基础层,同时加入不同应用文件。容器运行时,再在镜像之上拥有自己的可写层。区分镜像、共享文件层和容器可写状态,才能解释为什么镜像复用能够节省工作,却不会让某个容器里的日常编辑自动出现在所有邻居中。 依据原文历史样例说明机制。当前审核仍为已修订、执行受阻。本次绘图未执行 Docker 命令,未增加运行、页面、集成或发布验证。 坏掉的样子 直接把几个相关镜像显示的大小相加,可能高估它们合计占用的本地存储,因为共享内容可能被重复计数。传输大小、解包后的文件大小,以及快照存储实际分配的空间,也不是同一个指标。共享层确实支持复用,但不能在没有测量具体存储实现和统计口径时,声称节省了一个精确的磁盘容量。 另一个常见错误,是把容器里的修改视为镜像修改。在一个普通容器中安装软件包或修改文件,改变的是该容器的可写文件系统状态。从原镜像新建容器,不会自动继承这次修复。于是,手工修好运行中的服务后再替换实例,原故障可能重新出现,因为用于替换的产物从未包含那次修复。 共享层也不是通用的应用兼容性边界。两个应用即使基础层相同,仍可能使用不同配置、挂载数据、架构,或者在后续层加入不同库版本。应比较有序文件系统标识,以及被验证的实际行为。仅仅看到可读标签相同,无法确定运行的是哪些字节,也无法解释运行设置是否一致。 快速验证 先取得固定摘要的 Python 基础镜像,再使用 Python 和 Docker 构建器运行脚本。实测环境为 Docker 29.3.1、Linux arm64,镜像内 Python 为 3.11.16。实验创建两个相关镜像和两个临时容器,不使用容器网络,结束时只删除自身命名的容器和标签,不触碰其他实验或服务: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(["docker",*args],capture_output=True,text=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def inspect(image):return json.loads(docker("image","inspect",image).stdout)[0] name='xbcatm-layers-'+uuid.uuid4().hex[:10];tags=[name+':left',name+':right'];containers=[name+'-one',name+'-two'] try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory);(root/'shared.py').write_text('value=10\n# '+name+'\n') (root/'Dockerfile').write_text(f'FROM {BASE}\nCOPY shared.py /app/shared.py\nCOPY main.py /app/main.py\nCMD ["python", "/app/main.py"]\n') for tag,increment in zip(tags,[1,2]): (root/'main.py').write_text(f'from shared import value\nprint(value+{increment})\n') docker('build','--network','none','-t',tag,str(root)) left=inspect(tags[0])['RootFS']['Layers'];right=inspect(tags[1])['RootFS']['Layers'] assert left[:-1]==right[:-1] and left[-1]!=right[-1] assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='11' assert docker('run','--rm','--network','none',tags[1]).stdout.strip()=='12' print('two images share',len(left)-1,'filesystem layer identities and differ in the final application layer') for container in containers: docker('run','-d','--name',container,'--network','none',tags[0],'python','-c','import time;time.sleep(60)') docker('exec',containers[0],'python','-c',"from pathlib import Path;Path('/app/shared.py').write_text('value=100\\n')") assert docker('exec',containers[0],'python','/app/main.py').stdout.strip()=='101' assert docker('exec',containers[1],'python','/app/main.py').stdout.strip()=='11' assert '/app/shared.py' in docker('diff',containers[0]).stdout assert inspect(tags[0])['RootFS']['Layers']==left assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='11' print('a write in one container changes its writable layer while the sibling container and original image retain the old code') finally: docker('rm','-f',*containers,check=False);docker('image','rm',*tags,check=False) 两次构建复制相同的 shared.py,其中定义数值十。最后的 main.py 不同:一个加一,另一个加二,所以程序分别输出 11 与 12。镜像检查显示,在本次基础镜像和构建结构下,前五个文件系统层标识相同,最后的应用层标识不同。这同时提供文件身份和程序结果两类证据。 ...

三月 22, 2027 · 1 分钟 · XB Field Notes
选定程序输出从较大的构建阶段进入具有自身基础和必要运行文件的运行阶段;编译器与源码不会自动跟随。

多阶段构建需要经过测试的运行边界

多阶段 Dockerfile 可以在一个阶段编译软件,再把选定输出复制到另一个运行阶段。最终阶段继承自己的基础镜像和明确复制的文件,而不会自动继承构建器里的编译器、源码目录和软件包缓存。这建立了有用的边界,但作者仍必须识别程序真正执行时需要的一切。文件复制成功,只能证明文件已进入产物,不能证明运行环境已经完整。 坏掉的样子 即使 ELF 解释器和共享库缺失,COPY 仍可成功把动态链接程序放进 scratch 镜像。构建可能正常结束,因为复制不会执行程序,也不会验证全部运行依赖图。随后启动却可能报告 no such file or directory,尽管配置路径上的可执行文件确实存在。此时需要检查解释器和库,而不是只反复确认程序文件是否被复制。 脚本需要解释器,应用可能需要证书根或时区数据,还有程序只在特定请求路径上加载插件或资源。简单启动测试可能漏掉这些较晚发生的需求。选择小型运行基础镜像,只有在它满足应用真实契约时才有价值,包括受支持架构和必要文件路径。不能因为某个镜像最小,就默认它适合所有应用。 早期阶段不属于最终镜像的继承链,也不意味着它被安全擦除。构建缓存和明确导出的中间镜像,仍可能保留其内容。敏感构建输入需要正确的密钥传递方式,复制输出也必须检查是否意外包含不该分发的材料。多阶段结构控制最终文件系统组成,却不会自动保证整个构建过程的保密性。 窄范围 COPY 控制最终文件系统组成,既不验证运行依赖完整,也不会安全擦除构建缓存。实时软件包来源不能承诺编译器字节永久相同。Docker 示例执行仍受阻,本次未重跑。 快速验证 历史 Docker 示例保持原样;当前没有 Docker 运行环境,因此本次没有重跑。 使用 Python、Docker、固定的 Python 基础镜像,以及供构建阶段安装 Debian 软件包的网络运行脚本。运行容器关闭网络。实测是 Docker 29.3.1、Linux arm64,GCC 为 Debian 12.2.0-14+deb12u1 包提供的 12.2.0。软件仓库是实时仓库,因此实验验证运行组成,不承诺未来永远取得完全相同的编译器字节: ...

九月 25, 2026 · 2 分钟 · XB Field Notes