# Plan: APT Optimization & PM3 Build Caching

## Goal
Reduce redundant network requests and unnecessary rebuilds during pi-gen image builds.

---

## Part 1: Consolidate APT operations in stageDangerousPi

### 1A. Create a unified package list file for stageDangerousPi

**File:** `pi-gen/stageDangerousPi/00-packages` (new)

Move all apt packages currently installed inline across substages into a single
pi-gen `00-packages` file at the stage root level. Pi-gen's `run_sub_stage()`
already handles these automatically — one `apt-get install` call, one
`Reading package lists` parse.

**Problem:** pi-gen processes `XX-packages` files *per sub-stage directory*, not
at the stage level. The `run_stage()` iterates over `${STAGE_DIR}/*` sub-stage
directories, then `run_sub_stage()` looks for `{00..99}-packages` inside each.
There is no stage-level package consolidation built in.

**Revised approach:** Add a new sub-stage `00-apt-setup` that runs first (before
`01-Wireless-AP`) and handles all package installation for the entire
stageDangerousPi in one shot.

Create: `pi-gen/stageDangerousPi/00-apt-setup/00-packages`
```
# All packages needed across stageDangerousPi substages
# (consolidated to avoid redundant apt-get calls)

# 01-Wireless-AP
hostapd
dnsmasq
nftables
nginx

# 03-dangerous-pi
nodejs
npm
lrzsz

# 04-pisugar
i2c-tools
```

Note: `python3-pip` is omitted because it's already present from stage2 (Python 3.13
is installed in stage1, pip comes with it). The conditional `if ! command -v pip3`
in 03-dangerous-pi is dead code.

### 1B. Remove inline `apt-get install` calls from substage scripts

**Files to modify:**

1. `pi-gen/stageDangerousPi/01-Wireless-AP/00-run-chroot.sh`
   - Remove lines 12-16 (`apt-get install -y hostapd dnsmasq nftables nginx`)
   - Add a comment: `# Packages installed by 00-apt-setup stage`

2. `pi-gen/stageDangerousPi/03-dangerous-pi/00-run-chroot.sh`
   - Remove lines 17-25 (the conditional pip3 check + `apt-get update` + both
     `apt-get install` calls)
   - Add a comment noting packages come from 00-apt-setup

3. `pi-gen/stageDangerousPi/04-pisugar/00-run-chroot.sh`
   - Remove lines 49-50 (`apt-get update` + `apt-get install -y i2c-tools`)
   - Add a comment noting i2c-tools comes from 00-apt-setup

### 1C. Remove the redundant `apt-get update` from stagePM3

**File:** `pi-gen/stagePM3/01-proxmark3/00-run-chroot.sh`
- Remove line 11 (`apt-get update`)
- The metadata was just refreshed by stage0's `00-configure-apt`. Between
  stage0 and stagePM3, `copy_previous()` preserves `/var/lib/apt/lists/`
  (only `/var/cache/apt/archives` is excluded). The index is still valid.

**Risk assessment:** Low. The build runs in a single session — the metadata
fetched minutes ago in stage0 hasn't changed. The build log confirms all 4
repos returned `Hit:` (cache valid) when stagePM3 ran `apt-get update`.

### Estimated savings
- 2-3 redundant `apt-get update` round-trips eliminated: ~10-15s
- ~5 fewer `Reading package lists` parses under QEMU: ~25-40s
- Fewer apt-get install invocations (3→0 in stageDangerousPi): ~15-20s
- **Total: ~50-75s per build**

---

## Part 2: PM3 build caching (skip rebuild when unchanged)

### Problem
The PM3 build (clone + compile firmware + client + SWIG bindings) takes
~45-60 minutes under QEMU. Currently `from-pm3` always rebuilds it, even
if nothing changed (same upstream commit, same patch, same build deps).

### Approach: Content-addressed cache key

Create a manifest file that captures everything that affects the PM3 build
output. If the manifest matches what was used for the cached build, skip
the entire PM3 compilation.

**Cache key inputs:**
1. The LED PWM control patch file hash
2. The Proxmark3 upstream git commit (pinned or HEAD)
3. The branding sed command in the build script
4. The build script itself (00-run-chroot.sh)

### Implementation

#### 2A. Pin the Proxmark3 upstream commit

**File:** `pi-gen/stagePM3/01-proxmark3/00-run-chroot.sh`

Currently clones `HEAD` of master — non-deterministic. Change to clone a
specific tag or commit:

```bash
PM3_VERSION="v4.20728"  # Pin to known-good release
PM3_COMMIT=""           # Optional: specific commit hash

git clone https://github.com/RfidResearchGroup/proxmark3
cd proxmark3
if [ -n "$PM3_COMMIT" ]; then
    git checkout "$PM3_COMMIT"
fi
```

Add a small config file `pi-gen/stagePM3/pm3-version.conf`:
```bash
PM3_REPO="https://github.com/RfidResearchGroup/proxmark3"
PM3_COMMIT="ef82d5ba1"  # from the build log version string
```

#### 2B. Generate and check a build manifest

**File:** `pi-gen/stagePM3/01-proxmark3/00-run.sh` (pre-chroot, runs on host)

Add manifest generation and comparison logic:

```bash
# Generate cache key from all PM3 build inputs
generate_pm3_manifest() {
    {
        # Hash of the build script itself
        sha256sum "${SCRIPT_DIR}/00-run-chroot.sh"
        # Hash of the LED patch
        sha256sum "${SCRIPT_DIR}/led-pwm-control.patch"
        # PM3 version config
        cat "${SCRIPT_DIR}/../pm3-version.conf" 2>/dev/null || echo "HEAD"
    } | sha256sum | cut -d' ' -f1
}

MANIFEST=$(generate_pm3_manifest)
CACHED_MANIFEST="${ROOTFS_DIR}/opt/dangerous-pi/.pm3-build-manifest"

if [ -f "$CACHED_MANIFEST" ] && [ "$(cat "$CACHED_MANIFEST")" = "$MANIFEST" ]; then
    echo "PM3 build manifest unchanged — skipping rebuild"
    # Create a SKIP file for the chroot script
    touch "${SCRIPT_DIR}/SKIP_PM3_BUILD"
else
    echo "PM3 build manifest changed — will rebuild"
    echo "  New: $MANIFEST"
    [ -f "$CACHED_MANIFEST" ] && echo "  Old: $(cat "$CACHED_MANIFEST")"
    rm -f "${SCRIPT_DIR}/SKIP_PM3_BUILD"
fi
```

#### 2C. Honor the skip flag in the chroot build script

**File:** `pi-gen/stagePM3/01-proxmark3/00-run-chroot.sh`

Wrap the entire build in a check:

```bash
if [ -f /tmp/SKIP_PM3_BUILD ]; then
    echo "=== Skipping Proxmark3 build (cached, manifest unchanged) ==="
    # Still verify the installation is intact
    DEFAULT_USER=$(awk -F: '$3 >= 1000 && $3 < 65534 {print $1; exit}' /etc/passwd)
    DEFAULT_HOME=$(eval echo ~${DEFAULT_USER:-pi})
    if [ -f "$DEFAULT_HOME/.pm3/proxmark3/client/proxmark3" ]; then
        echo "✓ PM3 client binary present"
        exit 0
    else
        echo "✗ PM3 binary missing despite manifest match — rebuilding"
    fi
fi

# ... existing build logic ...
```

And at the end of a successful build, save the manifest:

```bash
# Save build manifest for future cache checks
mkdir -p /opt/dangerous-pi
# (manifest value is passed via a file from the pre-chroot script)
if [ -f /tmp/pm3-build-manifest ]; then
    cp /tmp/pm3-build-manifest /opt/dangerous-pi/.pm3-build-manifest
fi
```

#### 2D. Pass files between pre-chroot and chroot

**File:** `pi-gen/stagePM3/01-proxmark3/00-run.sh`

The pre-chroot script already copies the LED patch into `${ROOTFS_DIR}/tmp/`.
Extend it to also copy the skip flag and manifest value:

```bash
# Copy skip flag if set
if [ -f "${SCRIPT_DIR}/SKIP_PM3_BUILD" ]; then
    touch "${ROOTFS_DIR}/tmp/SKIP_PM3_BUILD"
    rm -f "${SCRIPT_DIR}/SKIP_PM3_BUILD"
fi

# Always write current manifest for the chroot to save on success
echo "$MANIFEST" > "${ROOTFS_DIR}/tmp/pm3-build-manifest"
```

#### 2E. Update build-image.sh for clarity

Add a `--force-pm3` flag to `from-pm3` mode that removes the cached manifest,
forcing a rebuild even if the inputs haven't changed:

```bash
from-pm3)
    # ... existing logic ...
    if [ "$FORCE_PM3" = "1" ]; then
        echo "Forcing PM3 rebuild (--force-pm3 flag)"
        # Remove cached manifest from the work directory rootfs
        rm -f "${PI_GEN_DIR}/work/${IMG_NAME}/stagePM3/rootfs/opt/dangerous-pi/.pm3-build-manifest"
    fi
```

And update the usage text to document the new behavior.

---

## Part 3: File changes summary

### New files
| File | Purpose |
|------|---------|
| `pi-gen/stageDangerousPi/00-apt-setup/00-packages` | Consolidated package list |
| `pi-gen/stagePM3/pm3-version.conf` | Pinned PM3 repo + commit |

### Modified files
| File | Change |
|------|--------|
| `pi-gen/stagePM3/01-proxmark3/00-run.sh` | Add manifest generation, skip flag, manifest passthrough |
| `pi-gen/stagePM3/01-proxmark3/00-run-chroot.sh` | Remove `apt-get update`; add skip check at top; save manifest on success; pin git clone to commit |
| `pi-gen/stageDangerousPi/01-Wireless-AP/00-run-chroot.sh` | Remove inline `apt-get install` |
| `pi-gen/stageDangerousPi/03-dangerous-pi/00-run-chroot.sh` | Remove conditional pip check, `apt-get update`, both `apt-get install` calls |
| `pi-gen/stageDangerousPi/04-pisugar/00-run-chroot.sh` | Remove `apt-get update` + `apt-get install` |
| `build-image.sh` | Add `--force-pm3` flag parsing and usage docs |

### Not modified (left as-is)
| File | Reason |
|------|--------|
| `pi-gen/stageDangerousPi/06-os-updates/00-packages` | Already uses file-based packages correctly |
| `pi-gen/stageDangerousPi/03-dangerous-pi/00-run-chroot.sh` cleanup section | `apt-get clean/autoclean/autoremove` at bottom is still useful |
| `pi-gen/stageDangerousPi/05-https-support/00-run-chroot.sh` | No apt calls to change |

---

## Risk assessment

| Change | Risk | Mitigation |
|--------|------|------------|
| Removing `apt-get update` from stagePM3 | Low — metadata inherited from stage0, same build session | Build log proves `Hit:` on all repos |
| Consolidating packages into 00-apt-setup | Low — same packages, just installed earlier | Substage ordering (00- before 01-) guarantees execution order |
| Removing conditional pip3 install | Low — pip3 always present from stage1/2 Python install | Verified in stage2 package lists |
| PM3 skip logic | Medium — if manifest check has a bug, stale binary ships | Verification step checks binary exists; `--force-pm3` escape hatch |
| Pinning PM3 commit | Low — explicit is better than implicit HEAD | Documented in pm3-version.conf; easy to bump |

## Testing approach

1. Run `./build-image.sh test` to syntax-check all modified scripts
2. Run `./build-image.sh from-dtpi` to verify APT consolidation works (Part 1)
3. Run `./build-image.sh from-pm3` twice — second run should skip PM3 build (Part 2)
4. Modify `led-pwm-control.patch` slightly, rebuild — should trigger PM3 rebuild
5. Run `./build-image.sh from-pm3 --force-pm3` — should always rebuild
