fix: don't stop bridge after setup to prevent macOS project corruption

Root cause: on macOS, `ghidra stop` triggers Ghidra's project close
which truncates .gpr to 0 bytes and leaves .rep with only metadata
stubs (~index.dat, project.prp) but no actual program data. This
causes all subsequent bridge starts to fail with "program file(s)
not found".

Changes:
- Remove `ghidra stop` from CI setup (corrupts macOS projects)
- Remove Step 3 bridge stop from ensure_test_project (same issue)
- Validate .gpr is non-empty AND idata/ has program data beyond
  just ~index.dat stubs
- Clean up stale project files before re-import
- Bump cache keys v3→v4 to invalidate broken caches
- Remove diagnostic output from test.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Kiselev
2026-02-22 15:42:46 -08:00
co-authored by Claude Opus 4.6
parent 876cdbd3cc
commit 6c2149c861
3 changed files with 33 additions and 43 deletions
+1 -2
View File
@@ -52,7 +52,7 @@ jobs:
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/projects
key: ghidra-projects-${{ matrix.os }}-v3-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
key: ghidra-projects-${{ matrix.os }}-v4-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
@@ -78,7 +78,6 @@ jobs:
run: |
cargo run -- import tests/fixtures/sample_binary --project ci-test --program sample_binary || true
cargo run -- analyze --project ci-test --program sample_binary || true
cargo run -- stop --project ci-test || true
shell: bash
timeout-minutes: 20
+4 -18
View File
@@ -90,7 +90,7 @@ jobs:
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/projects
key: ghidra-projects-${{ matrix.os }}-v3-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
key: ghidra-projects-${{ matrix.os }}-v4-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
@@ -116,20 +116,6 @@ jobs:
run: |
cargo run -- import tests/fixtures/sample_binary --project ci-test --program sample_binary || true
cargo run -- analyze --project ci-test --program sample_binary || true
cargo run -- stop --project ci-test || true
echo "=== Project directory contents ==="
if [ -d "$HOME/Library/Caches/ghidra-cli/projects" ]; then
find "$HOME/Library/Caches/ghidra-cli/projects" -type f -exec ls -la {} \; 2>/dev/null || true
du -sh "$HOME/Library/Caches/ghidra-cli/projects" 2>/dev/null || true
fi
if [ -d "$HOME/.cache/ghidra-cli/projects" ]; then
find "$HOME/.cache/ghidra-cli/projects" -type f -exec ls -la {} \; 2>/dev/null || true
du -sh "$HOME/.cache/ghidra-cli/projects" 2>/dev/null || true
fi
if [ -d "$HOME/AppData/Local/ghidra-cli/projects" ]; then
find "$HOME/AppData/Local/ghidra-cli/projects" -type f 2>/dev/null | head -20 || true
du -sh "$HOME/AppData/Local/ghidra-cli/projects" 2>/dev/null || true
fi
shell: bash
timeout-minutes: 20
@@ -178,7 +164,7 @@ jobs:
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/projects
key: ghidra-projects-${{ matrix.os }}-v3-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
key: ghidra-projects-${{ matrix.os }}-v4-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
@@ -251,7 +237,7 @@ jobs:
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/projects
key: ghidra-projects-${{ matrix.os }}-v3-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
key: ghidra-projects-${{ matrix.os }}-v4-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
@@ -324,7 +310,7 @@ jobs:
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/projects
key: ghidra-projects-${{ matrix.os }}-v3-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
key: ghidra-projects-${{ matrix.os }}-v4-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
+28 -23
View File
@@ -57,21 +57,35 @@ pub fn ensure_test_project(project: &str, program: &str) {
let gpr_file = projects_dir.join(format!("{}.gpr", project));
let rep_dir = projects_dir.join(format!("{}.rep", project));
// Check that .rep has actual content (not just an empty directory).
// If the bridge wasn't stopped before caching, .rep may exist but be empty
// because Ghidra hadn't flushed the project database to disk.
let rep_has_content = rep_dir.is_dir()
&& std::fs::read_dir(&rep_dir)
.map(|mut entries| entries.next().is_some())
// Validate project has actual program data, not just metadata stubs.
// On macOS, Ghidra's project close (during `ghidra stop`) can truncate
// .gpr to 0 bytes and leave .rep with only metadata stubs (~index.dat,
// project.prp) but no actual program data. A valid project needs:
// .gpr - non-empty project descriptor (XML)
// .rep/idata/ - must contain more than just ~index.dat (needs 00/ subdirectories)
let idata_dir = rep_dir.join("idata");
let idata_has_data = idata_dir.is_dir()
&& std::fs::read_dir(&idata_dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.file_name() != "~index.dat")
})
.unwrap_or(false);
let project_valid = gpr_file.exists()
&& gpr_file.metadata().map(|m| m.len() > 0).unwrap_or(false)
&& idata_has_data;
if gpr_file.exists() && rep_has_content {
if project_valid {
eprintln!("=== Using cached test project: {:?} ===", gpr_file);
return;
}
if gpr_file.exists() && !rep_has_content {
eprintln!("=== Project .gpr exists but .rep missing or empty, re-importing ===");
if gpr_file.exists() {
eprintln!("=== Project cache invalid (missing program data), re-importing ===");
// Remove stale project files to avoid conflicts during import
let _ = std::fs::remove_file(&gpr_file);
let _ = std::fs::remove_dir_all(&rep_dir);
}
eprintln!("=== Setting up test project (import + analyze) ===");
@@ -135,20 +149,11 @@ pub fn ensure_test_project(project: &str, program: &str) {
Err(e) => eprintln!("Analyze error: {}", e),
}
// Step 3: Stop the bridge that import/analyze left running.
// Without this, the first test reuses the leftover bridge (free pass),
// but when it stops the bridge, subsequent tests must start fresh ones.
// This inconsistency causes the first test to pass while others fail.
eprintln!("Step 3: Stopping leftover bridge...");
let stop_status = run_cli_with_timeout(
&ghidra_bin,
&["stop", "--project", project],
Duration::from_secs(60),
);
match stop_status {
Ok(status) => eprintln!("Bridge stop finished with status: {}", status),
Err(e) => eprintln!("Bridge stop error (may be expected): {}", e),
}
// NOTE: We intentionally do NOT stop the bridge here.
// On macOS, stopping the bridge triggers Ghidra's project close which
// can truncate .gpr to 0 bytes and destroy project data. Instead, we
// leave the bridge running. DaemonTestHarness::new() will detect the
// existing bridge via ensure_bridge_running() and reuse it.
eprintln!("=== Test project setup complete ===");
});