# Gradle Migration — Phase 1 Implementation Backlog

**Scope**: Make Gradle the canonical build backend for the plugin jar and extension ZIP.
**Acceptance gate**: `./gradlew buildExtension` produces a ZIP whose payload is byte-for-byte equivalent to Maven's `clean package assembly:single` output, and `tools.setup deploy` can consume it.

---

## Status of Phase 1 items

All build.gradle changes below are already applied. The one Python-side file change is open.

---

## Closed (applied in build.gradle)

### 1 — Remove GHIDRA_INSTALL_DIR throw at configuration time

**File**: `build.gradle`

**Before**: the script threw `GradleException` at configuration time when `GHIDRA_INSTALL_DIR` was absent, blocking every Gradle invocation including `./gradlew tasks` and `./gradlew verifyVersion`.

**After**: `ghidraInstallDirValue` is nullable. Tasks that need the install dir call `requireGhidraInstallDir` inside `doLast {}`, so task-selection and version-only tasks work without it.

---

### 2 — Move buildExtension destination from `dist/` to `build/distributions/`

**File**: `build.gradle` — `buildExtension` task

```groovy
// before
destinationDirectory = layout.projectDirectory.dir('dist')

// after
destinationDirectory = layout.buildDirectory.dir('distributions')
```

This gives `tools.setup deploy` a stable path under the standard Gradle output tree. The `dist/` directory is still cleaned by `cleanAll`.

---

### 3 — Fix manifest description and exclude static MANIFEST.MF

**File**: `build.gradle` — `jar` task

Changed `Plugin-Description` from `"195+ MCP tools"` to `"222 MCP tools"` to match `src/main/resources/META-INF/MANIFEST.MF`.

Added `exclude 'META-INF/MANIFEST.MF'` so the Gradle-generated manifest (which reads `project.version` dynamically) is the sole copy in the jar. The static file stays on disk for tooling that reads it directly, but is no longer embedded.

**Related follow-up (Phase 7)**: Update `src/main/resources/META-INF/MANIFEST.MF` to be a `.template` or to be generated by `processResources` so `bump-version` only has to update one file.

---

### 4 — Fix processResources pattern for version.properties

**File**: `build.gradle` — `processResources`

Changed `'version.properties'` to `'**/version.properties'`.

```groovy
// before — did NOT match com/xebyte/version.properties
filesMatching(['version.properties', 'extension.properties'])

// after — matches the file at its actual path
filesMatching(['**/version.properties', 'extension.properties'])
```

---

### 5 — Add `test { jvmArgs }` to match Maven surefire config

**File**: `build.gradle`

```groovy
test {
    jvmArgs '-Dnet.bytebuddy.experimental=true'
}
```

Mirrors `<argLine>-Dnet.bytebuddy.experimental=true</argLine>` in `pom.xml`. Without this, Mockito fails to instrument sealed/final Ghidra classes under Java 21.

---

### 6 — Add `verifyVersion` task (Phase 4 pre-work)

**File**: `build.gradle`

Reads `ghidra.version` from `pom.xml` and compares it against the version reported by `GHIDRA_INSTALL_DIR/Ghidra/application.properties`. Exits cleanly with no error if `GHIDRA_INSTALL_DIR` is not set.

```
./gradlew verifyVersion
./gradlew verifyVersion -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC
```

---

### 7 — Add `preflight` task (Phase 4 pre-work)

**File**: `build.gradle`

Validates Java on PATH, all 18 required Ghidra jars present, write access to both the Ghidra install extensions dir and the user-profile extensions dir. Depends on `verifyVersion`.

```
./gradlew preflight -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC
```

---

### 8 — Add `prepareGhidraClasspath` task (Phase 3 pre-work)

**File**: `build.gradle`

Checks that all 18 required Ghidra jars exist under `GHIDRA_INSTALL_DIR`. No `mvn install:install-file` step required — Gradle uses fileTree directly for compilation.

```
./gradlew prepareGhidraClasspath -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC
```

---

### 9 — Add Phase 5 deploy/launch task skeletons

**File**: `build.gradle`

Added `deployExtension`, `installUserExtension`, `patchGhidraUserConfig`, aggregate `deploy`, `startGhidra`, and `cleanAll`. The patch logic for `FrontEndTool.xml` and `_code_browser.tcd` is implemented in Groovy, mirroring `patch_frontend_tool_config()` and `patch_codebrowser_tcd()` in `tools/setup/ghidra.py`.

---

## Open — must complete before Phase 1 is fully wired

### 10 — Update `find_plugin_archive()` to search `build/distributions/`

**File**: `tools/setup/ghidra.py`  
**Function**: `find_plugin_archive` (line ~239)

`tools.setup deploy` calls `find_plugin_archive()` to locate the ZIP before copying it. It currently only searches `target/` (Maven output). After a Gradle build the ZIP is at `build/distributions/GhidraMCP-{version}.zip`.

**Change**:

```python
# before
def find_plugin_archive(repo_root: Path) -> Path:
    target_dir = repo_root / "target"
    version = read_pom_versions(repo_root).project_version
    candidates = [
        target_dir / f"GhidraMCP-{version}.zip",
        target_dir / "GhidraMCP.zip",
    ]
    for candidate in candidates:
        if candidate.is_file():
            return candidate
    archives = sorted(target_dir.glob("GhidraMCP*.zip"), ...)
    ...

# after — check Gradle output first, fall back to Maven target/
def find_plugin_archive(repo_root: Path) -> Path:
    version = read_pom_versions(repo_root).project_version
    candidates = [
        repo_root / "build" / "distributions" / f"GhidraMCP-{version}.zip",  # Gradle
        repo_root / "target" / f"GhidraMCP-{version}.zip",                    # Maven (transition)
        repo_root / "target" / "GhidraMCP.zip",
    ]
    for candidate in candidates:
        if candidate.is_file():
            return candidate
    # glob fallback — check Gradle output tree first
    for search_dir in [repo_root / "build" / "distributions", repo_root / "target"]:
        archives = sorted(search_dir.glob("GhidraMCP*.zip"), key=lambda p: p.stat().st_mtime, reverse=True)
        if archives:
            return archives[0]
    raise FileNotFoundError("No GhidraMCP plugin archive found in build/distributions/ or target/")
```

**Acceptance test**: run `./gradlew buildExtension`, then `python -m tools.setup deploy --ghidra-path F:\ghidra_12.1_PUBLIC`. Should find the zip and complete without error.

---

## Phase 1 acceptance checklist

Run these after applying item 10:

- [ ] `./gradlew buildExtension -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC` exits 0
- [ ] `build/distributions/GhidraMCP-5.4.1.zip` exists
- [ ] ZIP contains exactly: `GhidraMCP/extension.properties`, `GhidraMCP/Module.manifest`, `GhidraMCP/lib/GhidraMCP-5.4.1.jar` (no extra jars)
- [ ] `extension.properties` inside the ZIP has the substituted version (not `${project.version}`)
- [ ] `./gradlew test -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC` passes (offline unit tests)
- [ ] `./gradlew verifyVersion -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC` exits 0
- [ ] `./gradlew preflight -PGHIDRA_INSTALL_DIR=F:\ghidra_12.1_PUBLIC` exits 0
- [ ] `python -m tools.setup deploy --ghidra-path F:\ghidra_12.1_PUBLIC` finds the Gradle-produced ZIP

---

## What comes next (Phase 2 — CLI backend swap)

These are the only files that change in Phase 2. The Gradle task design is complete.

| File | Change |
|---|---|
| `tools/setup/maven.py` | Add `find_gradle_command()` and `run_gradle(repo_root, tasks, dry_run)` |
| `tools/setup/cli.py` | `cmd_build` → `run_gradle(..., ['buildExtension'])` |
| `tools/setup/cli.py` | `cmd_clean` → `run_gradle(..., ['clean'])` |
| `tools/setup/cli.py` | `cmd_run_tests` → `run_gradle(..., ['test'])` |
| `tools/setup/cli.py` | `cmd_install_ghidra_deps` → `run_gradle(..., ['prepareGhidraClasspath'])` |
| `tools/setup/cli.py` | `cmd_verify_version` → `run_gradle(..., ['verifyVersion'])` |
| `tools/setup/cli.py` | `cmd_preflight` → `run_gradle(..., ['preflight'])` |
| `tools/setup/cli.py` | `cmd_deploy` → `run_gradle(..., ['deploy'])` |
| `tools/setup/cli.py` | `cmd_start_ghidra` → `run_gradle(..., ['startGhidra'])` |

The `find_gradle_command()` resolver should check `./gradlew` (project wrapper, preferred), `GRADLE_HOME/bin/gradle`, and common install paths — same pattern as `find_maven_command()`.

Phase 2 does not change any public CLI flags or task IDs visible to users or VS Code tasks.
