mirror of
https://github.com/lifting-bits/sleigh
synced 2026-06-21 13:56:12 +00:00
Detect new files requiring manual intervention in Ghidra HEAD updates (#390)
Add functionality to the weekly sync workflow to detect newly added or deleted C++ source files and spec files that may require manual CMake configuration updates. When such files are detected, the PR body will include a warning section listing the files and instructions for maintainers. This addresses issue #121 where new files added to Sleigh in Ghidra weren't being noticed, leading to missing headers that required manual fixes later. Changes: - Add CategorizedChanges dataclass to track added/deleted cpp/spec files - Add helper methods to parse git status and categorize files - Expand IGNORED_EXTENSIONS to filter more non-relevant file types - Set new GitHub Actions outputs: needs_manual_intervention and intervention_details - Update workflow to conditionally add warning section to PR body Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,16 +70,35 @@ jobs:
|
|||||||
# Push branch
|
# Push branch
|
||||||
git push -u origin "$BRANCH"
|
git push -u origin "$BRANCH"
|
||||||
|
|
||||||
# Create PR
|
# Build PR body
|
||||||
gh pr create \
|
PR_BODY="Changed files:
|
||||||
--title "Update Ghidra HEAD to commit ${{ steps.head_update.outputs.short_sha }}" \
|
|
||||||
--body "$(cat <<'EOF'
|
|
||||||
Changed files:
|
|
||||||
|
|
||||||
${{ steps.head_update.outputs.changed_files }}
|
${{ steps.head_update.outputs.changed_files }}
|
||||||
|
|
||||||
Commit details:
|
Commit details:
|
||||||
|
|
||||||
${{ steps.head_update.outputs.commit_details }}
|
${{ steps.head_update.outputs.commit_details }}"
|
||||||
EOF
|
|
||||||
)"
|
# Add intervention warning if needed
|
||||||
|
if [ "${{ steps.head_update.outputs.needs_manual_intervention }}" = "true" ]; then
|
||||||
|
PR_BODY="## :warning: Manual Intervention Required
|
||||||
|
|
||||||
|
The following files were added or deleted and may require manual CMake configuration updates:
|
||||||
|
|
||||||
|
${{ steps.head_update.outputs.intervention_details }}
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
- **New C++ sources**: Add to appropriate list in \`src/setup-ghidra-source.cmake\`
|
||||||
|
- **Deleted C++ sources**: Remove from \`src/setup-ghidra-source.cmake\`
|
||||||
|
- **New spec files**: Review if \`.slaspec\` files are auto-generated; other types may need manual updates
|
||||||
|
- **Deleted spec files**: Verify no longer referenced
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
$PR_BODY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create PR
|
||||||
|
gh pr create \
|
||||||
|
--title "Update Ghidra HEAD to commit ${{ steps.head_update.outputs.short_sha }}" \
|
||||||
|
--body "$PR_BODY"
|
||||||
|
|||||||
+210
-15
@@ -8,6 +8,7 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Optional, Any, Tuple
|
from typing import List, Dict, Optional, Any, Tuple
|
||||||
|
|
||||||
@@ -23,12 +24,92 @@ SLEIGH_PATHS = [
|
|||||||
"Ghidra/Processors", # Sleigh files
|
"Ghidra/Processors", # Sleigh files
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# File extensions requiring manual CMake intervention
|
||||||
|
CPP_EXTENSIONS = {".cc", ".hh"}
|
||||||
|
SPEC_EXTENSIONS = {".slaspec", ".cspec", ".pspec", ".ldefs", ".opinion", ".sinc"}
|
||||||
|
|
||||||
|
# File extensions to ignore (in addition to .java)
|
||||||
|
IGNORED_EXTENSIONS = {
|
||||||
|
".java",
|
||||||
|
".gradle",
|
||||||
|
".properties",
|
||||||
|
".txt",
|
||||||
|
".md",
|
||||||
|
".html",
|
||||||
|
".xml",
|
||||||
|
".png",
|
||||||
|
".gif",
|
||||||
|
".jpg",
|
||||||
|
".ico",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Paths for categorizing files
|
||||||
|
CPP_PATH = "Ghidra/Features/Decompiler/src/decompile/cpp/"
|
||||||
|
SPEC_PATH_PREFIX = "Ghidra/Processors/"
|
||||||
|
|
||||||
# Regex patterns
|
# Regex patterns
|
||||||
HEAD_COMMIT_PATTERN = r"set\(ghidra_head_git_tag \"([0-9A-Fa-f]+)\"\)"
|
HEAD_COMMIT_PATTERN = r"set\(ghidra_head_git_tag \"([0-9A-Fa-f]+)\"\)"
|
||||||
VERSION_PATTERN = r"set\(ghidra_head_version \"([0-9]+(\.[0-9]+)*)\"\)"
|
VERSION_PATTERN = r"set\(ghidra_head_version \"([0-9]+(\.[0-9]+)*)\"\)"
|
||||||
APP_VERSION_PATTERN = r"application.version=([0-9]+(\.[0-9]+)*)"
|
APP_VERSION_PATTERN = r"application.version=([0-9]+(\.[0-9]+)*)"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CategorizedChanges:
|
||||||
|
"""Holds files categorized by change type and file type."""
|
||||||
|
|
||||||
|
added_cpp: List[str] = field(default_factory=list)
|
||||||
|
deleted_cpp: List[str] = field(default_factory=list)
|
||||||
|
added_spec: List[str] = field(default_factory=list)
|
||||||
|
deleted_spec: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def needs_manual_intervention(self) -> bool:
|
||||||
|
"""Check if any files need manual intervention."""
|
||||||
|
return bool(
|
||||||
|
self.added_cpp or self.deleted_cpp or self.added_spec or self.deleted_spec
|
||||||
|
)
|
||||||
|
|
||||||
|
def format_intervention_details(self) -> str:
|
||||||
|
"""Format the intervention details as markdown."""
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
if self.added_cpp:
|
||||||
|
sections.append("### New C++ Source Files")
|
||||||
|
sections.append(
|
||||||
|
"These files need to be added to `src/setup-ghidra-source.cmake`:"
|
||||||
|
)
|
||||||
|
for f in self.added_cpp:
|
||||||
|
sections.append(f"- `{f}`")
|
||||||
|
sections.append("")
|
||||||
|
|
||||||
|
if self.deleted_cpp:
|
||||||
|
sections.append("### Deleted C++ Source Files")
|
||||||
|
sections.append(
|
||||||
|
"These files need to be removed from `src/setup-ghidra-source.cmake`:"
|
||||||
|
)
|
||||||
|
for f in self.deleted_cpp:
|
||||||
|
sections.append(f"- `{f}`")
|
||||||
|
sections.append("")
|
||||||
|
|
||||||
|
if self.added_spec:
|
||||||
|
sections.append("### New Spec Files")
|
||||||
|
sections.append(
|
||||||
|
"Review if these files need manual CMake updates (`.slaspec` files "
|
||||||
|
"are auto-generated; other types may need manual updates):"
|
||||||
|
)
|
||||||
|
for f in self.added_spec:
|
||||||
|
sections.append(f"- `{f}`")
|
||||||
|
sections.append("")
|
||||||
|
|
||||||
|
if self.deleted_spec:
|
||||||
|
sections.append("### Deleted Spec Files")
|
||||||
|
sections.append("Verify these files are no longer referenced:")
|
||||||
|
for f in self.deleted_spec:
|
||||||
|
sections.append(f"- `{f}`")
|
||||||
|
sections.append("")
|
||||||
|
|
||||||
|
return "\n".join(sections).rstrip()
|
||||||
|
|
||||||
|
|
||||||
class GitHelper:
|
class GitHelper:
|
||||||
"""Helper class for Git operations"""
|
"""Helper class for Git operations"""
|
||||||
|
|
||||||
@@ -70,6 +151,60 @@ class GitHelper:
|
|||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _should_ignore_file(file_path: str) -> bool:
|
||||||
|
"""Check if a file should be ignored based on its extension."""
|
||||||
|
ext = Path(file_path).suffix.lower()
|
||||||
|
return ext in IGNORED_EXTENSIONS
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _categorize_file(
|
||||||
|
status: str, file_path: str, categorized: CategorizedChanges
|
||||||
|
) -> None:
|
||||||
|
"""Categorize a file based on its status and type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status: Git status code (A, D, M, R, etc.)
|
||||||
|
file_path: Path to the file
|
||||||
|
categorized: CategorizedChanges object to update
|
||||||
|
"""
|
||||||
|
ext = Path(file_path).suffix.lower()
|
||||||
|
|
||||||
|
# Only categorize added (A) or deleted (D) files
|
||||||
|
if status not in ("A", "D"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if it's a C++ file in the decompiler path
|
||||||
|
if ext in CPP_EXTENSIONS and CPP_PATH in file_path:
|
||||||
|
if status == "A":
|
||||||
|
categorized.added_cpp.append(file_path)
|
||||||
|
else: # status == "D"
|
||||||
|
categorized.deleted_cpp.append(file_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if it's a spec file in the Processors path
|
||||||
|
if ext in SPEC_EXTENSIONS and file_path.startswith(SPEC_PATH_PREFIX):
|
||||||
|
if status == "A":
|
||||||
|
categorized.added_spec.append(file_path)
|
||||||
|
else: # status == "D"
|
||||||
|
categorized.deleted_spec.append(file_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_git_status_line(line: str) -> Tuple[str, str, Optional[str]]:
|
||||||
|
"""Parse a git status line from --name-status output.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (status, file_path, new_path_for_rename)
|
||||||
|
"""
|
||||||
|
parts = line.split("\t")
|
||||||
|
status = parts[0]
|
||||||
|
|
||||||
|
# Handle rename (R100 or similar)
|
||||||
|
if status.startswith("R"):
|
||||||
|
return ("R", parts[1], parts[2])
|
||||||
|
|
||||||
|
return (status, parts[1], None)
|
||||||
|
|
||||||
def get_commit_info(
|
def get_commit_info(
|
||||||
self, repo_dir: Path, old_commit: str, new_commit: str, paths: List[str]
|
self, repo_dir: Path, old_commit: str, new_commit: str, paths: List[str]
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
@@ -119,8 +254,21 @@ class GitHelper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
commit_files = files_result.stdout.strip().splitlines()
|
commit_files = files_result.stdout.strip().splitlines()
|
||||||
# Filter out Java files
|
# Filter out ignored files
|
||||||
commit_files = [f for f in commit_files if not f.endswith(".java")]
|
filtered_files = []
|
||||||
|
for line in commit_files:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
status, file_path, new_path = self._parse_git_status_line(line)
|
||||||
|
if status == "R":
|
||||||
|
# For renames, check both old and new paths
|
||||||
|
if not self._should_ignore_file(file_path):
|
||||||
|
filtered_files.append(f"D\t{file_path}")
|
||||||
|
if new_path and not self._should_ignore_file(new_path):
|
||||||
|
filtered_files.append(f"A\t{new_path}")
|
||||||
|
elif not self._should_ignore_file(file_path):
|
||||||
|
filtered_files.append(line)
|
||||||
|
commit_files = filtered_files
|
||||||
|
|
||||||
if commit_files:
|
if commit_files:
|
||||||
commits.append(
|
commits.append(
|
||||||
@@ -137,8 +285,12 @@ class GitHelper:
|
|||||||
|
|
||||||
def get_changed_files(
|
def get_changed_files(
|
||||||
self, repo_dir: Path, old_commit: str, new_commit: str, paths: List[str]
|
self, repo_dir: Path, old_commit: str, new_commit: str, paths: List[str]
|
||||||
) -> List[str]:
|
) -> Tuple[List[str], CategorizedChanges]:
|
||||||
"""Get list of files changed between commits"""
|
"""Get list of files changed between commits and categorize them.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (filtered_files_list, categorized_changes)
|
||||||
|
"""
|
||||||
result = self.run(
|
result = self.run(
|
||||||
[
|
[
|
||||||
"diff",
|
"diff",
|
||||||
@@ -151,11 +303,29 @@ class GitHelper:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
changed_files = result.stdout.strip().splitlines()
|
raw_lines = result.stdout.strip().splitlines()
|
||||||
# Filter out Java files
|
filtered_files = []
|
||||||
changed_files = [f for f in changed_files if not f.endswith(".java")]
|
categorized = CategorizedChanges()
|
||||||
|
|
||||||
return changed_files
|
for line in raw_lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
status, file_path, new_path = self._parse_git_status_line(line)
|
||||||
|
|
||||||
|
if status == "R":
|
||||||
|
# For renames, treat as delete old + add new
|
||||||
|
if not self._should_ignore_file(file_path):
|
||||||
|
filtered_files.append(f"D\t{file_path}")
|
||||||
|
self._categorize_file("D", file_path, categorized)
|
||||||
|
if new_path and not self._should_ignore_file(new_path):
|
||||||
|
filtered_files.append(f"A\t{new_path}")
|
||||||
|
self._categorize_file("A", new_path, categorized)
|
||||||
|
elif not self._should_ignore_file(file_path):
|
||||||
|
filtered_files.append(line)
|
||||||
|
self._categorize_file(status, file_path, categorized)
|
||||||
|
|
||||||
|
return filtered_files, categorized
|
||||||
|
|
||||||
|
|
||||||
class GhidraUpdater:
|
class GhidraUpdater:
|
||||||
@@ -205,16 +375,20 @@ class GhidraUpdater:
|
|||||||
|
|
||||||
def display_changes(
|
def display_changes(
|
||||||
self, repo_dir: Path, start_commit: str, end_commit: str
|
self, repo_dir: Path, start_commit: str, end_commit: str
|
||||||
) -> Tuple[List[str], List[Dict[str, Any]]]:
|
) -> Tuple[List[str], List[Dict[str, Any]], CategorizedChanges]:
|
||||||
"""Display changes between two commits and return the changed files and commit info"""
|
"""Display changes between two commits and return the changed files and commit info.
|
||||||
# Get changed files
|
|
||||||
changed_files = self.git.get_changed_files(
|
Returns:
|
||||||
|
Tuple of (changed_files, commit_info, categorized_changes)
|
||||||
|
"""
|
||||||
|
# Get changed files and categorized changes
|
||||||
|
changed_files, categorized = self.git.get_changed_files(
|
||||||
repo_dir, start_commit, end_commit, SLEIGH_PATHS
|
repo_dir, start_commit, end_commit, SLEIGH_PATHS
|
||||||
)
|
)
|
||||||
|
|
||||||
if not changed_files:
|
if not changed_files:
|
||||||
print("No sleigh files were modified between these commits")
|
print("No sleigh files were modified between these commits")
|
||||||
return [], []
|
return [], [], CategorizedChanges()
|
||||||
|
|
||||||
# Output changes for logging
|
# Output changes for logging
|
||||||
num_changed = len(changed_files)
|
num_changed = len(changed_files)
|
||||||
@@ -222,6 +396,18 @@ class GhidraUpdater:
|
|||||||
for file in changed_files:
|
for file in changed_files:
|
||||||
print(f" {file}")
|
print(f" {file}")
|
||||||
|
|
||||||
|
# Display manual intervention warning if needed
|
||||||
|
if categorized.needs_manual_intervention():
|
||||||
|
print("\n** Manual intervention may be required **")
|
||||||
|
if categorized.added_cpp:
|
||||||
|
print(f" New C++ files: {len(categorized.added_cpp)}")
|
||||||
|
if categorized.deleted_cpp:
|
||||||
|
print(f" Deleted C++ files: {len(categorized.deleted_cpp)}")
|
||||||
|
if categorized.added_spec:
|
||||||
|
print(f" New spec files: {len(categorized.added_spec)}")
|
||||||
|
if categorized.deleted_spec:
|
||||||
|
print(f" Deleted spec files: {len(categorized.deleted_spec)}")
|
||||||
|
|
||||||
# Get detailed commit info for logging
|
# Get detailed commit info for logging
|
||||||
commit_info = self.git.get_commit_info(
|
commit_info = self.git.get_commit_info(
|
||||||
repo_dir, start_commit, end_commit, SLEIGH_PATHS
|
repo_dir, start_commit, end_commit, SLEIGH_PATHS
|
||||||
@@ -269,7 +455,16 @@ class GhidraUpdater:
|
|||||||
|
|
||||||
self.log_github_multiline_output("commit_details", "\n".join(details))
|
self.log_github_multiline_output("commit_details", "\n".join(details))
|
||||||
|
|
||||||
return changed_files, commit_info
|
# Log manual intervention outputs
|
||||||
|
if categorized.needs_manual_intervention():
|
||||||
|
self.log_github_output("needs_manual_intervention", "true")
|
||||||
|
self.log_github_multiline_output(
|
||||||
|
"intervention_details", categorized.format_intervention_details()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log_github_output("needs_manual_intervention", "false")
|
||||||
|
|
||||||
|
return changed_files, commit_info, categorized
|
||||||
|
|
||||||
def update_head_commit(
|
def update_head_commit(
|
||||||
self, repo_dir: Path, setup_file: Path
|
self, repo_dir: Path, setup_file: Path
|
||||||
@@ -298,7 +493,7 @@ class GhidraUpdater:
|
|||||||
print(f"Found new commit: {latest_commit}")
|
print(f"Found new commit: {latest_commit}")
|
||||||
|
|
||||||
# Check if sleigh files were updated and display changes
|
# Check if sleigh files were updated and display changes
|
||||||
changed_files, commit_info = self.display_changes(
|
changed_files, commit_info, _ = self.display_changes(
|
||||||
repo_dir, current_commit, latest_commit
|
repo_dir, current_commit, latest_commit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user