fix(startup): externalize comparison evidence
- commit
- 1dd2cab14bbe684b30ed206d29ed4ee3a6d52e4e
- author
- brice · · 2026-07-14 09:53 UTC
- parent
- a948576
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +114 -19
M docs/first-presentation.md +18 -9
| @@ -25,17 +25,26 @@ Build the candidate, then run: | |||
| 25 | 25 | ||
| 26 | 26 | ```bash | |
| 27 | 27 | DUMBER_FIRST_PRESENTATION_BIN="$PWD/dist/dumber" \ | |
| 28 | - | DUMBER_FIRST_PRESENTATION_OUTPUT="$PWD/phase1/first-presentation-$(date +%s)" \ | |
| 28 | + | DUMBER_MACHINE_GPU_PROFILE=integrated-gpu \ | |
| 29 | 29 | scripts/collect_first_presentation.sh | |
| 30 | 30 | ``` | |
| 31 | 31 | ||
| 32 | - | The output directory must be a new, absolute directory below an existing | |
| 33 | - | non-symlink parent. The collector refuses to clear or reuse a caller-supplied | |
| 34 | - | path; review and publish a fresh collection explicitly. The script requires the current display and the CEF 147 runtime at | |
| 32 | + | By default each collection is a fresh directory below | |
| 33 | + | `$XDG_STATE_HOME/dumber/roadmap-evidence` (or | |
| 34 | + | `$HOME/.local/state/dumber/roadmap-evidence` when `XDG_STATE_HOME` is unset), | |
| 35 | + | not in the repository. `DUMBER_FIRST_PRESENTATION_OUTPUT` may override it only | |
| 36 | + | with a new absolute directory below an existing non-symlink parent. The | |
| 37 | + | collector never clears or reuses a caller-supplied path. | |
| 38 | + | ||
| 39 | + | The script requires the current display and the CEF 147 runtime at | |
| 35 | 40 | `~/.local/share/cef-147-runtime` (override with `DUMBER_CEF_DIR`). It performs | |
| 36 | 41 | exactly five bounded launches with fresh XDG homes and CEF root cache, fixes the | |
| 37 | - | DMABUF/Vulkan environment, and writes `metadata.json`, `run-01.json` through | |
| 38 | - | `run-05.json`, and `baseline.json` below `phase1/first-presentation`. Published | |
| 39 | - | artifacts contain only logical labels, version/hash identifiers, and timing | |
| 40 | - | fields; raw logs and temporary XDG homes are removed. A missing or incomplete | |
| 41 | - | timeline, non-DMABUF backend, or invalid run fails collection. | |
| 42 | + | DMABUF/Vulkan renderer, and writes exactly `metadata.json`, `run-01.json` | |
| 43 | + | through `run-05.json`, and `baseline.json`. Metadata includes only coarse, | |
| 44 | + | comparison-safe OS/architecture, display protocol, and machine/GPU profile | |
| 45 | + | labels. Set `DUMBER_MACHINE_GPU_PROFILE` to one of `generic-gpu`, | |
| 46 | + | `integrated-gpu`, `discrete-gpu`, `hybrid-gpu`, `virtual-gpu`, or `unknown-gpu`; | |
| 47 | + | do not use a device model or other identifier. Publish only the seven reviewed | |
| 48 | + | JSON files to an external Gist. Raw logs and temporary XDG homes are removed and | |
| 49 | + | must not be published. A missing or incomplete timeline, non-DMABUF backend, or | |
| 50 | + | invalid run fails collection. | |
| @@ -25,17 +25,26 @@ Build the candidate, then run: | |||||
| 25 | 25 | ||||
| 26 | ```bash | 26 | ```bash | ||
| 27 | DUMBER_FIRST_PRESENTATION_BIN="$PWD/dist/dumber" \ | 27 | DUMBER_FIRST_PRESENTATION_BIN="$PWD/dist/dumber" \ | ||
| 28 | - | DUMBER_FIRST_PRESENTATION_OUTPUT="$PWD/phase1/first-presentation-$(date +%s)" \ | 28 | + | DUMBER_MACHINE_GPU_PROFILE=integrated-gpu \ |
| 29 | scripts/collect_first_presentation.sh | 29 | scripts/collect_first_presentation.sh | ||
| 30 | ``` | 30 | ``` | ||
| 31 | 31 | ||||
| 32 | - | The output directory must be a new, absolute directory below an existing | 32 | + | By default each collection is a fresh directory below |
| 33 | - | non-symlink parent. The collector refuses to clear or reuse a caller-supplied | 33 | + | `$XDG_STATE_HOME/dumber/roadmap-evidence` (or |
| 34 | - | path; review and publish a fresh collection explicitly. The script requires the current display and the CEF 147 runtime at | 34 | + | `$HOME/.local/state/dumber/roadmap-evidence` when `XDG_STATE_HOME` is unset), |
| 35 | + | not in the repository. `DUMBER_FIRST_PRESENTATION_OUTPUT` may override it only | |||
| 36 | + | with a new absolute directory below an existing non-symlink parent. The | |||
| 37 | + | collector never clears or reuses a caller-supplied path. | |||
| 38 | + | ||||
| 39 | + | The script requires the current display and the CEF 147 runtime at | |||
| 35 | `~/.local/share/cef-147-runtime` (override with `DUMBER_CEF_DIR`). It performs | 40 | `~/.local/share/cef-147-runtime` (override with `DUMBER_CEF_DIR`). It performs | ||
| 36 | exactly five bounded launches with fresh XDG homes and CEF root cache, fixes the | 41 | exactly five bounded launches with fresh XDG homes and CEF root cache, fixes the | ||
| 37 | - | DMABUF/Vulkan environment, and writes `metadata.json`, `run-01.json` through | 42 | + | DMABUF/Vulkan renderer, and writes exactly `metadata.json`, `run-01.json` |
| 38 | - | `run-05.json`, and `baseline.json` below `phase1/first-presentation`. Published | 43 | + | through `run-05.json`, and `baseline.json`. Metadata includes only coarse, |
| 39 | - | artifacts contain only logical labels, version/hash identifiers, and timing | 44 | + | comparison-safe OS/architecture, display protocol, and machine/GPU profile |
| 40 | - | fields; raw logs and temporary XDG homes are removed. A missing or incomplete | 45 | + | labels. Set `DUMBER_MACHINE_GPU_PROFILE` to one of `generic-gpu`, |
| 41 | - | timeline, non-DMABUF backend, or invalid run fails collection. | 46 | + | `integrated-gpu`, `discrete-gpu`, `hybrid-gpu`, `virtual-gpu`, or `unknown-gpu`; |
| 47 | + | do not use a device model or other identifier. Publish only the seven reviewed | |||
| 48 | + | JSON files to an external Gist. Raw logs and temporary XDG homes are removed and | |||
| 49 | + | must not be published. A missing or incomplete timeline, non-DMABUF backend, or | |||
| 50 | + | invalid run fails collection. | |||
M internal/logging/first_presentation_collector_test.go +62 -0
| @@ -122,10 +122,12 @@ func TestFirstPresentationCollectorSanitizesMachineLocalValues(t *testing.T) { | |||
| 122 | 122 | cmd.Dir = repoRoot | |
| 123 | 123 | cmd.Env = append(os.Environ(), | |
| 124 | 124 | "DISPLAY=:test", | |
| 125 | + | "WAYLAND_DISPLAY=", | |
| 125 | 126 | "DUMBER_CEF_DIR="+runtime, | |
| 126 | 127 | "DUMBER_FIRST_PRESENTATION_BIN="+binary, | |
| 127 | 128 | "DUMBER_FIRST_PRESENTATION_OUTPUT="+output, | |
| 128 | 129 | "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1", | |
| 130 | + | "DUMBER_MACHINE_GPU_PROFILE=integrated-gpu", | |
| 129 | 131 | ) | |
| 130 | 132 | result, err := cmd.CombinedOutput() | |
| 131 | 133 | require.NoErrorf(t, err, "collector failed: %s", result) | |
| @@ -146,6 +148,66 @@ func TestFirstPresentationCollectorSanitizesMachineLocalValues(t *testing.T) { | |||
| 146 | 148 | } { | |
| 147 | 149 | require.Contains(t, artifacts.String(), required) | |
| 148 | 150 | } | |
| 151 | + | ||
| 152 | + | var metadata struct { | |
| 153 | + | Comparison struct { | |
| 154 | + | OS string `json:"os"` | |
| 155 | + | Architecture string `json:"architecture"` | |
| 156 | + | DisplayProtocol string `json:"display_protocol"` | |
| 157 | + | MachineGPUProfile string `json:"machine_gpu_profile"` | |
| 158 | + | } `json:"comparison"` | |
| 159 | + | } | |
| 160 | + | contents, err := os.ReadFile(filepath.Join(output, "metadata.json")) | |
| 161 | + | require.NoError(t, err) | |
| 162 | + | require.NoError(t, json.Unmarshal(contents, &metadata)) | |
| 163 | + | require.NotEmpty(t, metadata.Comparison.OS) | |
| 164 | + | require.NotEmpty(t, metadata.Comparison.Architecture) | |
| 165 | + | require.Equal(t, "x11", metadata.Comparison.DisplayProtocol) | |
| 166 | + | require.Equal(t, "integrated-gpu", metadata.Comparison.MachineGPUProfile) | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func TestFirstPresentationCollectorDefaultsToXDGStateEvidenceDirectory(t *testing.T) { | |
| 170 | + | repoRoot, err := filepath.Abs(filepath.Join("..", "..")) | |
| 171 | + | require.NoError(t, err) | |
| 172 | + | temp := t.TempDir() | |
| 173 | + | runtime := filepath.Join(temp, "cef-147-runtime") | |
| 174 | + | require.NoError(t, os.Mkdir(runtime, 0o755)) | |
| 175 | + | binary := filepath.Join(temp, "dumber") | |
| 176 | + | require.NoError(t, os.WriteFile(binary, collectorTestBinary(validFirstPresentationLog), 0o755)) | |
| 177 | + | stateHome := filepath.Join(temp, "state") | |
| 178 | + | ||
| 179 | + | cmd := exec.Command(filepath.Join(repoRoot, "scripts", "collect_first_presentation.sh")) | |
| 180 | + | cmd.Dir = repoRoot | |
| 181 | + | cmd.Env = append(envWithout("DUMBER_FIRST_PRESENTATION_OUTPUT"), | |
| 182 | + | "DISPLAY=:test", | |
| 183 | + | "WAYLAND_DISPLAY=", | |
| 184 | + | "DUMBER_CEF_DIR="+runtime, | |
| 185 | + | "DUMBER_FIRST_PRESENTATION_BIN="+binary, | |
| 186 | + | "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1", | |
| 187 | + | "DUMBER_MACHINE_GPU_PROFILE=integrated-gpu", | |
| 188 | + | "XDG_STATE_HOME="+stateHome, | |
| 189 | + | ) | |
| 190 | + | result, err := cmd.CombinedOutput() | |
| 191 | + | require.NoErrorf(t, err, "collector failed: %s", result) | |
| 192 | + | ||
| 193 | + | evidenceRoot := filepath.Join(stateHome, "dumber", "roadmap-evidence") | |
| 194 | + | entries, err := os.ReadDir(evidenceRoot) | |
| 195 | + | require.NoError(t, err) | |
| 196 | + | require.Len(t, entries, 1) | |
| 197 | + | require.True(t, entries[0].IsDir()) | |
| 198 | + | require.FileExists(t, filepath.Join(evidenceRoot, entries[0].Name(), "metadata.json")) | |
| 199 | + | require.NotContains(t, string(result), filepath.Join(repoRoot, "phase1")) | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | func envWithout(name string) []string { | |
| 203 | + | prefix := name + "=" | |
| 204 | + | var environment []string | |
| 205 | + | for _, entry := range os.Environ() { | |
| 206 | + | if !strings.HasPrefix(entry, prefix) { | |
| 207 | + | environment = append(environment, entry) | |
| 208 | + | } | |
| 209 | + | } | |
| 210 | + | return environment | |
| 149 | 211 | } | |
| 150 | 212 | ||
| 151 | 213 | func TestFirstPresentationCollectorReadsProvenanceWithScopedGitSafeDirectory(t *testing.T) { | |
| @@ -122,10 +122,12 @@ func TestFirstPresentationCollectorSanitizesMachineLocalValues(t *testing.T) { | |||||
| 122 | cmd.Dir = repoRoot | 122 | cmd.Dir = repoRoot | ||
| 123 | cmd.Env = append(os.Environ(), | 123 | cmd.Env = append(os.Environ(), | ||
| 124 | "DISPLAY=:test", | 124 | "DISPLAY=:test", | ||
| 125 | + | "WAYLAND_DISPLAY=", | |||
| 125 | "DUMBER_CEF_DIR="+runtime, | 126 | "DUMBER_CEF_DIR="+runtime, | ||
| 126 | "DUMBER_FIRST_PRESENTATION_BIN="+binary, | 127 | "DUMBER_FIRST_PRESENTATION_BIN="+binary, | ||
| 127 | "DUMBER_FIRST_PRESENTATION_OUTPUT="+output, | 128 | "DUMBER_FIRST_PRESENTATION_OUTPUT="+output, | ||
| 128 | "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1", | 129 | "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1", | ||
| 130 | + | "DUMBER_MACHINE_GPU_PROFILE=integrated-gpu", | |||
| 129 | ) | 131 | ) | ||
| 130 | result, err := cmd.CombinedOutput() | 132 | result, err := cmd.CombinedOutput() | ||
| 131 | require.NoErrorf(t, err, "collector failed: %s", result) | 133 | require.NoErrorf(t, err, "collector failed: %s", result) | ||
| @@ -146,6 +148,66 @@ func TestFirstPresentationCollectorSanitizesMachineLocalValues(t *testing.T) { | |||||
| 146 | } { | 148 | } { | ||
| 147 | require.Contains(t, artifacts.String(), required) | 149 | require.Contains(t, artifacts.String(), required) | ||
| 148 | } | 150 | } | ||
| 151 | + | ||||
| 152 | + | var metadata struct { | |||
| 153 | + | Comparison struct { | |||
| 154 | + | OS string `json:"os"` | |||
| 155 | + | Architecture string `json:"architecture"` | |||
| 156 | + | DisplayProtocol string `json:"display_protocol"` | |||
| 157 | + | MachineGPUProfile string `json:"machine_gpu_profile"` | |||
| 158 | + | } `json:"comparison"` | |||
| 159 | + | } | |||
| 160 | + | contents, err := os.ReadFile(filepath.Join(output, "metadata.json")) | |||
| 161 | + | require.NoError(t, err) | |||
| 162 | + | require.NoError(t, json.Unmarshal(contents, &metadata)) | |||
| 163 | + | require.NotEmpty(t, metadata.Comparison.OS) | |||
| 164 | + | require.NotEmpty(t, metadata.Comparison.Architecture) | |||
| 165 | + | require.Equal(t, "x11", metadata.Comparison.DisplayProtocol) | |||
| 166 | + | require.Equal(t, "integrated-gpu", metadata.Comparison.MachineGPUProfile) | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func TestFirstPresentationCollectorDefaultsToXDGStateEvidenceDirectory(t *testing.T) { | |||
| 170 | + | repoRoot, err := filepath.Abs(filepath.Join("..", "..")) | |||
| 171 | + | require.NoError(t, err) | |||
| 172 | + | temp := t.TempDir() | |||
| 173 | + | runtime := filepath.Join(temp, "cef-147-runtime") | |||
| 174 | + | require.NoError(t, os.Mkdir(runtime, 0o755)) | |||
| 175 | + | binary := filepath.Join(temp, "dumber") | |||
| 176 | + | require.NoError(t, os.WriteFile(binary, collectorTestBinary(validFirstPresentationLog), 0o755)) | |||
| 177 | + | stateHome := filepath.Join(temp, "state") | |||
| 178 | + | ||||
| 179 | + | cmd := exec.Command(filepath.Join(repoRoot, "scripts", "collect_first_presentation.sh")) | |||
| 180 | + | cmd.Dir = repoRoot | |||
| 181 | + | cmd.Env = append(envWithout("DUMBER_FIRST_PRESENTATION_OUTPUT"), | |||
| 182 | + | "DISPLAY=:test", | |||
| 183 | + | "WAYLAND_DISPLAY=", | |||
| 184 | + | "DUMBER_CEF_DIR="+runtime, | |||
| 185 | + | "DUMBER_FIRST_PRESENTATION_BIN="+binary, | |||
| 186 | + | "DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS=1", | |||
| 187 | + | "DUMBER_MACHINE_GPU_PROFILE=integrated-gpu", | |||
| 188 | + | "XDG_STATE_HOME="+stateHome, | |||
| 189 | + | ) | |||
| 190 | + | result, err := cmd.CombinedOutput() | |||
| 191 | + | require.NoErrorf(t, err, "collector failed: %s", result) | |||
| 192 | + | ||||
| 193 | + | evidenceRoot := filepath.Join(stateHome, "dumber", "roadmap-evidence") | |||
| 194 | + | entries, err := os.ReadDir(evidenceRoot) | |||
| 195 | + | require.NoError(t, err) | |||
| 196 | + | require.Len(t, entries, 1) | |||
| 197 | + | require.True(t, entries[0].IsDir()) | |||
| 198 | + | require.FileExists(t, filepath.Join(evidenceRoot, entries[0].Name(), "metadata.json")) | |||
| 199 | + | require.NotContains(t, string(result), filepath.Join(repoRoot, "phase1")) | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | func envWithout(name string) []string { | |||
| 203 | + | prefix := name + "=" | |||
| 204 | + | var environment []string | |||
| 205 | + | for _, entry := range os.Environ() { | |||
| 206 | + | if !strings.HasPrefix(entry, prefix) { | |||
| 207 | + | environment = append(environment, entry) | |||
| 208 | + | } | |||
| 209 | + | } | |||
| 210 | + | return environment | |||
| 149 | } | 211 | } | ||
| 150 | 212 | ||||
| 151 | func TestFirstPresentationCollectorReadsProvenanceWithScopedGitSafeDirectory(t *testing.T) { | 213 | func TestFirstPresentationCollectorReadsProvenanceWithScopedGitSafeDirectory(t *testing.T) { | ||
M scripts/collect_first_presentation.sh +34 -10
| @@ -8,21 +8,32 @@ readonly runs=5 | |||
| 8 | 8 | readonly timeout_seconds="${DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS:-45}" | |
| 9 | 9 | readonly runtime="${DUMBER_CEF_DIR:-$HOME/.local/share/cef-147-runtime}" | |
| 10 | 10 | readonly binary="${DUMBER_FIRST_PRESENTATION_BIN:-$PWD/dist/dumber}" | |
| 11 | + | ||
| 12 | + | fail_unsafe_output() { | |
| 13 | + | echo "first-presentation: unsafe output path: $1" >&2 | |
| 14 | + | exit 2 | |
| 15 | + | } | |
| 16 | + | ||
| 11 | 17 | if [[ -v DUMBER_FIRST_PRESENTATION_OUTPUT ]]; then | |
| 12 | 18 | output="$DUMBER_FIRST_PRESENTATION_OUTPUT" | |
| 13 | 19 | else | |
| 14 | - | output="$PWD/phase1/first-presentation" | |
| 20 | + | # Evidence is state, not source: keep the default outside the checkout and | |
| 21 | + | # make every invocation a fresh child of the external evidence directory. | |
| 22 | + | readonly state_home="${XDG_STATE_HOME:-$HOME/.local/state}" | |
| 23 | + | [[ "$state_home" == /* ]] || fail_unsafe_output "XDG_STATE_HOME must be absolute" | |
| 24 | + | readonly evidence_root="$state_home/dumber/roadmap-evidence" | |
| 25 | + | mkdir -p -- "$evidence_root" || fail_unsafe_output "could not create XDG state directory" | |
| 26 | + | canonical_evidence_root="$(realpath -e -- "$evidence_root")" || \ | |
| 27 | + | fail_unsafe_output "XDG state directory cannot be canonicalized" | |
| 28 | + | [[ "$canonical_evidence_root" == "$evidence_root" && ! -L "$evidence_root" ]] || \ | |
| 29 | + | fail_unsafe_output "XDG state directory must not contain symbolic links" | |
| 30 | + | output="$evidence_root/first-presentation-$(date -u +%Y%m%dT%H%M%S)-$$" | |
| 15 | 31 | fi | |
| 16 | 32 | readonly output | |
| 17 | 33 | readonly upstream_module="github.com/bnema/purego-cef2gtk" | |
| 18 | 34 | readonly upstream_tag="v0.8.4" | |
| 19 | 35 | readonly upstream_revision="f217ece342dea3ef2a3f98671fcd16a39ad0037d" | |
| 20 | 36 | ||
| 21 | - | fail_unsafe_output() { | |
| 22 | - | echo "first-presentation: unsafe output path: $1" >&2 | |
| 23 | - | exit 2 | |
| 24 | - | } | |
| 25 | - | ||
| 26 | 37 | # The artifact destination is caller-controlled. Never empty or recursively | |
| 27 | 38 | # clear it: collection only writes to a newly-created directory whose parent is | |
| 28 | 39 | # already canonical and contains no symbolic-link hop. | |
| @@ -62,25 +73,38 @@ prepare_output | |||
| 62 | 73 | ||
| 63 | 74 | # Raw logs and temporary XDG homes may contain machine-local paths. Keep them | |
| 64 | 75 | # outside the committed artifact directory and always remove them. | |
| 65 | - | readonly work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")" | |
| 76 | + | work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")" | |
| 77 | + | readonly work_root | |
| 66 | 78 | cleanup_work_root() { | |
| 67 | 79 | [[ -n "${work_root:-}" && -d "$work_root" && ! -L "$work_root" ]] || return | |
| 68 | 80 | rm -rf -- "$work_root" | |
| 69 | 81 | } | |
| 70 | 82 | trap cleanup_work_root EXIT | |
| 71 | 83 | python3 - "$output/metadata.json" "$binary" "$timeout_seconds" "$upstream_module" "$upstream_tag" "$upstream_revision" <<'PY' | |
| 72 | - | import hashlib, json, os, subprocess, sys | |
| 84 | + | import hashlib, json, os, platform, subprocess, sys | |
| 73 | 85 | path, binary, timeout, upstream_module, upstream_tag, upstream_revision = sys.argv[1:] | |
| 74 | 86 | with open(binary, "rb") as candidate: | |
| 75 | 87 | binary_sha256 = hashlib.file_digest(candidate, "sha256").hexdigest() | |
| 88 | + | ||
| 89 | + | # These deliberately coarse labels support comparison without exposing a host, | |
| 90 | + | # device name, driver version, path, or environment dump. | |
| 91 | + | architecture = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower(), "other") | |
| 92 | + | os_label = {"linux": "linux", "darwin": "darwin", "windows": "windows"}.get(platform.system().lower(), "other") | |
| 93 | + | display_protocol = "wayland" if os.environ.get("WAYLAND_DISPLAY") else "x11" | |
| 94 | + | gpu_profile = os.environ.get("DUMBER_MACHINE_GPU_PROFILE", "generic-gpu") | |
| 95 | + | allowed_gpu_profiles = {"generic-gpu", "integrated-gpu", "discrete-gpu", "hybrid-gpu", "virtual-gpu", "unknown-gpu"} | |
| 96 | + | if gpu_profile not in allowed_gpu_profiles: | |
| 97 | + | raise SystemExit("DUMBER_MACHINE_GPU_PROFILE must be a non-identifying profile label") | |
| 98 | + | ||
| 76 | 99 | json.dump({ | |
| 77 | 100 | "runs": 5, | |
| 78 | - | "runtime": {"label": "cef", "version": os.environ.get("DUMBER_CEF_RUNTIME_VERSION", "147")}, | |
| 101 | + | "runtime": {"label": "cef", "version": "147"}, | |
| 79 | 102 | "binary": {"label": "dumber", "sha256": binary_sha256}, | |
| 80 | 103 | "timeout_seconds": int(timeout), | |
| 81 | 104 | "measured_source_revision": subprocess.check_output(["git", "-c", f"safe.directory={os.getcwd()}", "rev-parse", "HEAD"], text=True).strip(), | |
| 82 | 105 | "upstream": {"module": upstream_module, "tag": upstream_tag, "revision": upstream_revision}, | |
| 83 | - | "fixed_environment": {"DUMBER_RENDER_STACK": "vulkan-dmabuf", "PUREGO_CEF2GTK_BACKEND": "gdk-dmabuf", "GSK_RENDERER": "vulkan"} | |
| 106 | + | "comparison": {"os": os_label, "architecture": architecture, "display_protocol": display_protocol, "machine_gpu_profile": gpu_profile}, | |
| 107 | + | "render_configuration": {"backend": "gdk-dmabuf", "buffer_sharing": "dmabuf", "renderer": "vulkan"} | |
| 84 | 108 | }, open(path, "w"), indent=2, sort_keys=True) | |
| 85 | 109 | PY | |
| 86 | 110 | ||
| @@ -8,21 +8,32 @@ readonly runs=5 | |||||
| 8 | readonly timeout_seconds="${DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS:-45}" | 8 | readonly timeout_seconds="${DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS:-45}" | ||
| 9 | readonly runtime="${DUMBER_CEF_DIR:-$HOME/.local/share/cef-147-runtime}" | 9 | readonly runtime="${DUMBER_CEF_DIR:-$HOME/.local/share/cef-147-runtime}" | ||
| 10 | readonly binary="${DUMBER_FIRST_PRESENTATION_BIN:-$PWD/dist/dumber}" | 10 | readonly binary="${DUMBER_FIRST_PRESENTATION_BIN:-$PWD/dist/dumber}" | ||
| 11 | + | ||||
| 12 | + | fail_unsafe_output() { | |||
| 13 | + | echo "first-presentation: unsafe output path: $1" >&2 | |||
| 14 | + | exit 2 | |||
| 15 | + | } | |||
| 16 | + | ||||
| 11 | if [[ -v DUMBER_FIRST_PRESENTATION_OUTPUT ]]; then | 17 | if [[ -v DUMBER_FIRST_PRESENTATION_OUTPUT ]]; then | ||
| 12 | output="$DUMBER_FIRST_PRESENTATION_OUTPUT" | 18 | output="$DUMBER_FIRST_PRESENTATION_OUTPUT" | ||
| 13 | else | 19 | else | ||
| 14 | - | output="$PWD/phase1/first-presentation" | 20 | + | # Evidence is state, not source: keep the default outside the checkout and |
| 21 | + | # make every invocation a fresh child of the external evidence directory. | |||
| 22 | + | readonly state_home="${XDG_STATE_HOME:-$HOME/.local/state}" | |||
| 23 | + | [[ "$state_home" == /* ]] || fail_unsafe_output "XDG_STATE_HOME must be absolute" | |||
| 24 | + | readonly evidence_root="$state_home/dumber/roadmap-evidence" | |||
| 25 | + | mkdir -p -- "$evidence_root" || fail_unsafe_output "could not create XDG state directory" | |||
| 26 | + | canonical_evidence_root="$(realpath -e -- "$evidence_root")" || \ | |||
| 27 | + | fail_unsafe_output "XDG state directory cannot be canonicalized" | |||
| 28 | + | [[ "$canonical_evidence_root" == "$evidence_root" && ! -L "$evidence_root" ]] || \ | |||
| 29 | + | fail_unsafe_output "XDG state directory must not contain symbolic links" | |||
| 30 | + | output="$evidence_root/first-presentation-$(date -u +%Y%m%dT%H%M%S)-$$" | |||
| 15 | fi | 31 | fi | ||
| 16 | readonly output | 32 | readonly output | ||
| 17 | readonly upstream_module="github.com/bnema/purego-cef2gtk" | 33 | readonly upstream_module="github.com/bnema/purego-cef2gtk" | ||
| 18 | readonly upstream_tag="v0.8.4" | 34 | readonly upstream_tag="v0.8.4" | ||
| 19 | readonly upstream_revision="f217ece342dea3ef2a3f98671fcd16a39ad0037d" | 35 | readonly upstream_revision="f217ece342dea3ef2a3f98671fcd16a39ad0037d" | ||
| 20 | 36 | ||||
| 21 | - | fail_unsafe_output() { | |||
| 22 | - | echo "first-presentation: unsafe output path: $1" >&2 | |||
| 23 | - | exit 2 | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | # The artifact destination is caller-controlled. Never empty or recursively | 37 | # The artifact destination is caller-controlled. Never empty or recursively | ||
| 27 | # clear it: collection only writes to a newly-created directory whose parent is | 38 | # clear it: collection only writes to a newly-created directory whose parent is | ||
| 28 | # already canonical and contains no symbolic-link hop. | 39 | # already canonical and contains no symbolic-link hop. | ||
| @@ -62,25 +73,38 @@ prepare_output | |||||
| 62 | 73 | ||||
| 63 | # Raw logs and temporary XDG homes may contain machine-local paths. Keep them | 74 | # Raw logs and temporary XDG homes may contain machine-local paths. Keep them | ||
| 64 | # outside the committed artifact directory and always remove them. | 75 | # outside the committed artifact directory and always remove them. | ||
| 65 | - | readonly work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")" | 76 | + | work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")" |
| 77 | + | readonly work_root | |||
| 66 | cleanup_work_root() { | 78 | cleanup_work_root() { | ||
| 67 | [[ -n "${work_root:-}" && -d "$work_root" && ! -L "$work_root" ]] || return | 79 | [[ -n "${work_root:-}" && -d "$work_root" && ! -L "$work_root" ]] || return | ||
| 68 | rm -rf -- "$work_root" | 80 | rm -rf -- "$work_root" | ||
| 69 | } | 81 | } | ||
| 70 | trap cleanup_work_root EXIT | 82 | trap cleanup_work_root EXIT | ||
| 71 | python3 - "$output/metadata.json" "$binary" "$timeout_seconds" "$upstream_module" "$upstream_tag" "$upstream_revision" <<'PY' | 83 | python3 - "$output/metadata.json" "$binary" "$timeout_seconds" "$upstream_module" "$upstream_tag" "$upstream_revision" <<'PY' | ||
| 72 | - | import hashlib, json, os, subprocess, sys | 84 | + | import hashlib, json, os, platform, subprocess, sys |
| 73 | path, binary, timeout, upstream_module, upstream_tag, upstream_revision = sys.argv[1:] | 85 | path, binary, timeout, upstream_module, upstream_tag, upstream_revision = sys.argv[1:] | ||
| 74 | with open(binary, "rb") as candidate: | 86 | with open(binary, "rb") as candidate: | ||
| 75 | binary_sha256 = hashlib.file_digest(candidate, "sha256").hexdigest() | 87 | binary_sha256 = hashlib.file_digest(candidate, "sha256").hexdigest() | ||
| 88 | + | ||||
| 89 | + | # These deliberately coarse labels support comparison without exposing a host, | |||
| 90 | + | # device name, driver version, path, or environment dump. | |||
| 91 | + | architecture = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower(), "other") | |||
| 92 | + | os_label = {"linux": "linux", "darwin": "darwin", "windows": "windows"}.get(platform.system().lower(), "other") | |||
| 93 | + | display_protocol = "wayland" if os.environ.get("WAYLAND_DISPLAY") else "x11" | |||
| 94 | + | gpu_profile = os.environ.get("DUMBER_MACHINE_GPU_PROFILE", "generic-gpu") | |||
| 95 | + | allowed_gpu_profiles = {"generic-gpu", "integrated-gpu", "discrete-gpu", "hybrid-gpu", "virtual-gpu", "unknown-gpu"} | |||
| 96 | + | if gpu_profile not in allowed_gpu_profiles: | |||
| 97 | + | raise SystemExit("DUMBER_MACHINE_GPU_PROFILE must be a non-identifying profile label") | |||
| 98 | + | ||||
| 76 | json.dump({ | 99 | json.dump({ | ||
| 77 | "runs": 5, | 100 | "runs": 5, | ||
| 78 | - | "runtime": {"label": "cef", "version": os.environ.get("DUMBER_CEF_RUNTIME_VERSION", "147")}, | 101 | + | "runtime": {"label": "cef", "version": "147"}, |
| 79 | "binary": {"label": "dumber", "sha256": binary_sha256}, | 102 | "binary": {"label": "dumber", "sha256": binary_sha256}, | ||
| 80 | "timeout_seconds": int(timeout), | 103 | "timeout_seconds": int(timeout), | ||
| 81 | "measured_source_revision": subprocess.check_output(["git", "-c", f"safe.directory={os.getcwd()}", "rev-parse", "HEAD"], text=True).strip(), | 104 | "measured_source_revision": subprocess.check_output(["git", "-c", f"safe.directory={os.getcwd()}", "rev-parse", "HEAD"], text=True).strip(), | ||
| 82 | "upstream": {"module": upstream_module, "tag": upstream_tag, "revision": upstream_revision}, | 105 | "upstream": {"module": upstream_module, "tag": upstream_tag, "revision": upstream_revision}, | ||
| 83 | - | "fixed_environment": {"DUMBER_RENDER_STACK": "vulkan-dmabuf", "PUREGO_CEF2GTK_BACKEND": "gdk-dmabuf", "GSK_RENDERER": "vulkan"} | 106 | + | "comparison": {"os": os_label, "architecture": architecture, "display_protocol": display_protocol, "machine_gpu_profile": gpu_profile}, |
| 107 | + | "render_configuration": {"backend": "gdk-dmabuf", "buffer_sharing": "dmabuf", "renderer": "vulkan"} | |||
| 84 | }, open(path, "w"), indent=2, sort_keys=True) | 108 | }, open(path, "w"), indent=2, sort_keys=True) | ||
| 85 | PY | 109 | PY | ||
| 86 | 110 | ||||