fix: bytecode caches are derived data (not in the manifest, not checked); the smoke keeps the artifact pristine

This commit is contained in:
aphrody-dev committed 2026-10-05 23:13:23 +02:00
1 parent f0743b5266
commit 7c3db18909
5 files changed
+161 -4

No files matched your search

@@ -0,0 +1,117 @@
{
"schema": "aphrody.vu-forge/1",
"startedAt": "2026-10-05T21:10:38.090Z",
"finishedAt": "2026-10-05T21:10:42.756Z",
"ok": true,
"revision": "f0743b526600068f0181d579b53e72a87fd2731d",
"head": "f0743b526600068f0181d579b53e72a87fd2731d",
"dirty": false,
"host": "vps",
"toolchain": "rustc 1.98.1 (48a229cea 2026-09-01)",
"pins": {
"uv": {
"tag": "0.12.23",
"ref": "46b84fd0bfec23b72f29e8e2185ba68a65052f48"
},
"ruff": {
"tag": "0.16.10",
"ref": "3265ed1f944c98bb4c04d632fbefb1257cdb583d"
},
"pyo3": {
"tag": "v0.29.3",
"ref": "451d99fdcdcddf159e8e0a1186960b332cdb5d7c"
}
},
"steps": [
{
"name": "tunnel",
"ok": true,
"ms": 13
},
{
"name": "sync",
"ok": true,
"ms": 350,
"detail": "f0743b526600"
},
{
"name": "fetch",
"ok": true,
"ms": 904,
"detail": "python 3.12.15 731af898886c5f821890dc901eca3c651cca8e51fa7308c159d12a1194aeac91 -> /srv/aphrody-build/workspaces/vu-wsl/build/python"
},
{
"name": "assemble",
"ok": true,
"ms": 1404,
"detail": "/srv/aphrody-build/artifacts/vu/0.1.0-f0743b52"
},
{
"name": "smoke",
"ok": true,
"ms": 1862
}
],
"artifact": {
"artifact": "/srv/aphrody-build/artifacts/vu/0.1.0-f0743b52",
"files": 3494,
"links": 1049
},
"smoke": {
"ok": true,
"checks": [
{
"name": "manifest",
"ok": true,
"detail": "every file matches",
"ms": 1097
},
{
"name": "version",
"ok": true,
"detail": "{\"schema\":\"aphrody.vu/1\",\"vu\":\"0.1.0\",\"uv\":\"uv 0.12.23 (46b84fd0b 2026-10-03 x86_64-unknown-linux-gnu)\",\"ruff\":\"ruff 0.16.10\",\"python\":\"3.12.15\",\"pyo3\":\"0.29.3\",\"target\":\"x86_64-unknown-linux-gnu\"}",
"ms": 29
},
{
"name": "uv sidecar",
"ok": true,
"detail": "uv 0.12.23 (46b84fd0b 2026-10-03 x86_64-unknown-linux-gnu)",
"ms": 18
},
{
"name": "ruff sidecar",
"ok": true,
"detail": "ruff 0.16.10; check exit 1",
"ms": 19
},
{
"name": "python parity",
"ok": true,
"detail": "identical sys.version, sys.prefix, sys.executable, sys.path and extension modules",
"ms": 240
},
{
"name": "python exit status and stdin",
"ok": true,
"detail": "exit 7, stdin result 42",
"ms": 31
},
{
"name": "sdk wheel through the sidecar uv and the embedded interpreter",
"ok": true,
"detail": "aphrody.aphrody_rust.cosine_similarity returned 1.0 inside vu python",
"ms": 298
},
{
"name": "glibc audit",
"ok": true,
"detail": "vu: 2.39, uv: 2.39, ruff: 2.39, libpython3.12.so.1.0: 2.17; highest 2.39; within 2.39",
"ms": 46
}
],
"wheel": {
"path": "/srv/aphrody-build/artifacts/wsl/ir-wheel/dist/aphrody-0.1.0-cp311-abi3-linux_x86_64.whl",
"sha256": "f90db352507324e5c55ea3c9e0d646e1ba2704f3102d946cce3ea791baf44eee"
}
}
}
+2
View File
@@ -118,6 +118,8 @@ export async function mapLimit<T, R>(
let next = 0;
const worker = async (): Promise<void> => {
for (let index = next++; index < items.length; index = next++) {
// The workers are the concurrency: each one runs its items in sequence.
// eslint-disable-next-line no-await-in-loop
results[index] = await fn(items[index] as T);
}
};
+23 -1
View File
@@ -11,7 +11,13 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildManifest, MANIFEST_PATH, type ManifestMeta, verifyManifest } from "./manifest.ts";
import {
buildManifest,
isDerivedPath,
MANIFEST_PATH,
type ManifestMeta,
verifyManifest,
} from "./manifest.ts";
const meta: ManifestMeta = {
schema: 1,
@@ -77,6 +83,22 @@ describe("manifest", () => {
expect(problems).toContain("link target: lib/libpython3.12.so");
});
test("bytecode caches are derived data: never listed, never required, never flagged", async () => {
expect(isDerivedPath("lib/python3.12/__pycache__/os.cpython-312.pyc")).toBe(true);
expect(isDerivedPath("__pycache__/x.pyc")).toBe(true);
expect(isDerivedPath("lib/python3.12/os.py")).toBe(false);
expect(isDerivedPath("lib/my__pycache__/x")).toBe(false);
const dir = prefix();
mkdirSync(join(dir, "lib", "__pycache__"), { recursive: true });
writeFileSync(join(dir, "lib", "__pycache__", "a.cpython-312.pyc"), "before");
const manifest = await buildManifest(dir, meta);
expect(Object.keys(manifest.files).some((path) => path.includes("__pycache__"))).toBe(false);
// CPython writes and rewrites caches while the runtime is used.
writeFileSync(join(dir, "lib", "__pycache__", "a.cpython-312.pyc"), "after, longer");
writeFileSync(join(dir, "lib", "__pycache__", "b.cpython-312.pyc"), "new");
expect(await verifyManifest(dir, manifest)).toEqual([]);
});
test("another schema or name is refused", async () => {
const dir = prefix();
const manifest = await buildManifest(dir, meta);
+12 -2
View File
@@ -9,6 +9,14 @@ import { mapLimit, sha256File } from "./lib.ts";
export const MANIFEST_PATH = "share/vu/manifest.json";
/**
* Bytecode caches are derived data: CPython writes them into the runtime when it runs, so they are neither listed
* in the manifest nor checked by a consumer (the sources and the binaries they derive from are).
*/
export function isDerivedPath(path: string): boolean {
return /(^|\/)__pycache__\//.test(path);
}
export interface FileEntry {
sha256: string;
bytes: number;
@@ -48,7 +56,7 @@ export async function buildManifest(prefix: string, meta: ManifestMeta): Promise
walk(prefix, prefix, listing);
const files: Record<string, FileEntry> = {};
const entries = await mapLimit(
listing.files.filter((path) => path !== MANIFEST_PATH),
listing.files.filter((path) => path !== MANIFEST_PATH && !isDerivedPath(path)),
32,
async (path) => {
const stat = lstatSync(join(prefix, path));
@@ -76,7 +84,9 @@ export async function verifyManifest(prefix: string, manifest?: Manifest): Promi
return ["unsupported manifest schema or name"];
const listing = { files: [] as string[], links: [] as string[] };
walk(prefix, prefix, listing);
const present = new Set(listing.files.filter((path) => path !== MANIFEST_PATH));
const present = new Set(
listing.files.filter((path) => path !== MANIFEST_PATH && !isDerivedPath(path)),
);
for (const [path, entry] of Object.entries(declared.files)) {
if (!present.has(path)) {
problems.push(`missing: ${path}`);
+7 -1
View File
@@ -44,7 +44,13 @@ export function smokePlan(
options: SmokeOptions,
): { name: string; run: () => Promise<{ ok: boolean; detail: string }> }[] {
const bin = (name: string): string => join(prefix, "bin", name);
const environment = { VU_RUNTIME: prefix, UV_PYTHON_DOWNLOADS: "never", UV_NO_CONFIG: "1" };
// The artifact must stay pristine while it is checked: no bytecode caches are written into it.
const environment = {
VU_RUNTIME: prefix,
UV_PYTHON_DOWNLOADS: "never",
UV_NO_CONFIG: "1",
PYTHONDONTWRITEBYTECODE: "1",
};
return [
{
name: "manifest",