---
name: c-incremental-build-converter
description: Convert C/C++ project build.sh and test.sh scripts to support incremental builds. Use when fixing build scripts for re-execution after docker commit, converting to out-of-tree builds, or making scripts idempotent for repeated execution.
allowed-tools: Read, Grep, Glob, Bash, Edit, Write
---

# C/C++ Incremental Build Converter Skill

This skill converts C/C++ project build.sh and test.sh scripts to support incremental builds after docker commit.

## CRITICAL CONSTRAINTS

### 1. DO NOT Modify oss-fuzz Infrastructure Code

**NEVER modify these files:**
- `oss-fuzz/infra/helper.py`
- `oss-fuzz/infra/base-images/*`
- Any other oss-fuzz infrastructure code

**ONLY modify:**
- `oss-fuzz/projects/{project}/build.sh`
- `oss-fuzz/projects/{project}/test.sh`
- `oss-fuzz/projects/{project}/Dockerfile` (if absolutely necessary)

### 2. If oss-crs Test Framework Has Issues

If the incremental build test framework (`incremental_build_checker.py`) has bugs or limitations:
1. **Report the issue to the user** with clear explanation
2. **Suggest a fix** for the framework code
3. **DO NOT implement the fix yourself** - let the user decide

Example report format:
```
## oss-crs Framework Issue Detected

**File:** bug_fixing/src/oss_patch/inc_build_checker/incremental_build_checker.py
**Issue:** The source copy command destroys build artifacts
**Current behavior:** `rm -rf $SRC/nginx && cp -r ...`
**Expected behavior:** Should preserve objs/ directory
**Suggested fix:** Use rsync with --exclude objs instead of rm+cp
```

## Key Architecture: Separate $SRC for build.sh and test.sh

**IMPORTANT: $SRC is different between build.sh and test.sh.**

- build.sh runs in `/built-src` → `$SRC` = `/built-src`
- test.sh runs in `/test-src` → `$SRC` = `/test-src`

These are completely separate directories. This separation means:
1. **NO artifact cleanup needed** - Don't delete .o, .lo, .a, .la, .libs
2. **NO out-of-tree build needed** - In-source build is fine since $SRC is separate
3. **NO ASAN conflict** - test.sh builds in its own clean copy

### ALWAYS Use $SRC, NEVER Hardcode Paths

**CRITICAL: Always use `$SRC` variable, never hardcode `/src`, `/built-src`, or `/test-src`.**

```bash
# BAD - Hardcoded path breaks when $SRC changes
cd /src/project
cp -r /src/oniguruma modules/

# GOOD - Uses $SRC variable
cd $SRC/project
cp -r $SRC/oniguruma modules/
```

### AVOID $WORK - Use $SRC Subdirectories for All Build Artifacts

**CRITICAL: Avoid using $WORK for build-related directories. Use $SRC subdirectories instead.**

`$WORK` is shared between build.sh and test.sh, which causes problems:
1. ASAN-instrumented libraries from build.sh conflict with test.sh
2. CMake paths recorded in $WORK become invalid when $SRC changes
3. Build artifacts from different environments can corrupt each other

**Always use `$SRC` subdirectories for:**
- Build directories (out-of-source builds)
- Install prefixes (for cmake/configure --prefix)
- Dependency installations

```bash
# BAD - Uses $WORK which is shared
BUILD_DIR="$WORK/build"
cmake "$SRC/project" -DCMAKE_INSTALL_PREFIX=$WORK

# GOOD - Uses $SRC subdirectories
BUILD_DIR="$SRC/project_build"
INSTALL_DIR="$SRC/project_install"
mkdir -p "$BUILD_DIR" "$INSTALL_DIR"
cd "$BUILD_DIR"
cmake "$SRC/project" -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR"
```

**In test.sh, also use $SRC subdirectories:**

```bash
# BAD in test.sh - Uses shared $WORK with ASAN artifacts
./configure --prefix="$WORK"
# Error: undefined reference to __asan_*

# GOOD in test.sh - Uses separate prefix under $SRC
TEST_PREFIX="$SRC/test_deps"
mkdir -p "$TEST_PREFIX/lib" "$TEST_PREFIX/include"

if [ ! -f "$TEST_PREFIX/lib/libz.a" ]; then
    pushd "$SRC/zlib"
    ./configure --static --prefix="$TEST_PREFIX"
    make -j$(nproc) CFLAGS="-fPIC"  # No ASAN flags
    make install
    popd
fi
```

### test.sh Does NOT Need Sanitizers - Disable Them Explicitly

**IMPORTANT: test.sh runs unit tests, NOT fuzzing. Sanitizers are NOT needed and cause conflicts.**

During incremental build testing, build.sh compiles with ASAN/sanitizer flags. When test.sh runs in the SAME directory, the object files from build.sh are ASAN-instrumented. If test.sh tries to link against these objects without ASAN runtime, you get undefined reference errors.

### build.sh and test.sh Artifacts Should NOT Be Shared

**CRITICAL: Build artifacts from build.sh should NOT be reused in test.sh.**

During incremental build testing:
1. build.sh runs with ASAN flags → creates ASAN-instrumented `.o` files
2. test.sh runs in the SAME `$SRC` directory
3. If test.sh tries to use existing `.o` files, linking fails

**The artifacts are incompatible because:**
- build.sh: Compiles with `-fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link`
- test.sh: Needs to link without sanitizer runtime


**DO NOT use `|| true` to ignore link failures** - this hides the real problem. Fix the root cause by ensuring clean separation of build artifacts.

## Critical Requirements

### 1. Scripts MUST Work on BOTH First Run AND Repeated Runs

**NEVER** assume a script will only run once. Scripts must be idempotent.

```bash
# BAD - Runs configure every time
autoreconf -f -i
./configure

# GOOD - Only configure on first run
if [ ! -f Makefile ]; then
    autoreconf -f -i
    ./configure
fi
make -j$(nproc)
```

### 2. DO NOT Delete Build Artifacts

Since $SRC is separate, there's no need to clean artifacts. Deleting them defeats incremental build.

```bash
# BAD - Destroys incremental build benefits
find . -name "*.o" -delete
find . -name "*.a" -delete
rm -rf .libs

# GOOD - Just build, artifacts from previous run will speed things up
if [ ! -f Makefile ]; then
    ./configure
fi
make -j$(nproc)
```

### 3. Conditional Configure/Autoreconf

Only run configure-related commands on first execution.

```bash
# Pattern: Check for Makefile existence
if [ ! -f Makefile ]; then
    autoreconf -fi       # or ./bootstrap
    ./configure --options
fi
make -j$(nproc)
make check
```

### 4. Use `cp` Instead of `mv`

`mv` removes the source file, causing failures on repeated runs.

```bash
# BAD - Fails on second run
mv scripts/config.temp scripts/config

# GOOD - Preserves source file
[ -f scripts/config.temp ] && cp scripts/config.temp scripts/config || true
```

### 5. Use Guards for Dependencies

```bash
# Build dependency only if not already built
if [ ! -f "$WORK/lib/libz.a" ]; then
    pushd "$SRC/zlib"
    ./configure --static --prefix="$WORK"
    make -j$(nproc)
    make install
    popd
fi
```

### 6. Handle Failing Tests

Remove failing test files directly instead of using TESTS variable (which can propagate to submodules).

```bash
# BAD - TESTS variable propagates to submodules
make check TESTS="tests/mantest tests/jqtest"

# GOOD - Remove failing test files directly
rm -f tests/shtest
rm -f tests/multiple.*
sed -i '/multiple/d' tests/Makefile.am
make check
```

### 7. Copy Dependencies If Not Present (Conditional, Not Delete-and-Copy)

```bash
# BAD - Deletes and recopies every time, breaks incremental build
rm -rf modules/oniguruma
rsync -a $SRC/oniguruma modules/

# GOOD - Only copy if not present
if [ ! -d modules/oniguruma ]; then
    mkdir -p modules
    cp -r $SRC/oniguruma modules/
fi
```

### 8. Conditional Dict/Corpus File Copy

Files like dictionaries and seed corpus may not exist in all builds.

```bash
# BAD - Fails if file doesn't exist
cp $SRC/libtiff/contrib/oss-fuzz/tiff.dict $OUT/

# GOOD - Conditional copy with fallback
[ -f $SRC/libtiff/contrib/oss-fuzz/tiff.dict ] && cp $SRC/libtiff/contrib/oss-fuzz/tiff.dict $OUT/ || true
find . -name "*.tif" | xargs zip $OUT/seed_corpus.zip || true
```

### 9. Use `make -k check || true` for Potentially Failing Tests

Some tests may fail in fuzzing environment. Use `-k` to continue and `|| true` to not fail the script.

```bash
# BAD - Stops on first test failure
make check

# GOOD - Continue through failures
make -k check || true

echo "Tests completed"
```

### 10. Remove Tests from Makefile.am BEFORE configure

When removing failing tests, modify Makefile.am BEFORE running autoreconf/configure.

```bash
# BAD - Removes test file but Makefile still references it
rm -f tests/shtest
autoreconf -fi
./configure
make check  # Error: No rule to make target 'tests/shtest'

# GOOD - Remove from Makefile.am before configure
sed -i 's| tests/shtest||g' Makefile.am
autoreconf -fi
./configure
make check  # Works
```

### 11. Rewrite Instead of Sourcing Internal build.sh

If the project's source contains its own build.sh (e.g., `contrib/oss-fuzz/build.sh`), it may use `mv` or other non-idempotent operations. Rewrite the logic in the project's build.sh instead.

```bash
# BAD - Sources internal build.sh that uses mv
source $SRC/project/contrib/oss-fuzz/build.sh

# GOOD - Rewrite the logic with cp instead of mv
if [ ! -f "$WORK/lib/libjbig.a" ]; then
    pushd "$SRC/jbigkit"
    make lib
    # Use cp instead of mv for incremental build support
    cp "$SRC"/jbigkit/libjbig/*.a "$WORK/lib/"
    cp "$SRC"/jbigkit/libjbig/*.h "$WORK/include/"
    popd
fi
```

### 12. Guard `sed -i` Insertions with Pattern Checks

When using `sed -i` to insert content, check BOTH the original pattern exists AND the new content doesn't exist yet.

```bash
# BAD - Only checks if new content exists (can't distinguish original vs added)
if ! grep -q '%destructor' file.y; then
    sed -i '/^%%$/i%destructor { free ($$); } ID' file.y
fi

# GOOD - Check original pattern exists AND new content not yet added
if grep -q '^%%$' file.y && ! grep -q '%destructor' file.y; then
    sed -i '/^%%$/i%destructor { free ($$); } ID' file.y
fi
```

### 13. Guard Append Operations (`>>`)

Don't blindly use `>` (overwrite) or `>>` (append). Check if content already exists.

```bash
# BAD - Overwrites every time (inefficient, may lose state)
sed 's/main/old_main/' file.c > header.h

# BAD - Appends every time (file grows on each run)
sed 's/main/old_main/' file.c >> header.h

# GOOD - Only append if content not already present
if ! grep -q 'old_main' header.h 2>/dev/null; then
    sed 's/main/old_main/' file.c >> header.h
fi
```

### 14. CMake/Ninja Build Directory Must Use $SRC-Relative Path

When `$SRC` changes between build.sh and test.sh, CMake/Ninja will fail with "manifest dirty" errors if build directory is in `$WORK`.

```bash
# BAD - $WORK is shared, CMake source path mismatch causes ninja errors
BUILD_DIR="$WORK/build"
cd "$BUILD_DIR"
cmake $SRC/project  # Source path recorded as /built-src/project
# On test.sh: ninja fails because $SRC is now /test-src/project

# GOOD - Build directory relative to $SRC, each environment independent
BUILD_DIR="$SRC/project_build"
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
if [ ! -f build.ninja ]; then
    cmake -GNinja $SRC/project
fi
ninja
```

### 15. Disable ccache for Bazel Builds

Bazel has its own caching mechanism. ccache interferes with Bazel's compiler wrappers.

```bash
# BAD - ccache causes "invalid option" errors with Bazel
# Error: /usr/local/bin/ccache: invalid option -- 'U'
bazel build //...

# GOOD - Remove ccache from PATH before Bazel builds
export PATH=$(echo "$PATH" | sed 's|/ccache/bin:||g; s|:/ccache/bin||g')
unset CC CXX
export CC=clang
export CXX=clang++
bazel build //...
```

## Standard test.sh Template

**IMPORTANT: Execution Environment**
- `$SRC` is **already set** (no need for `: "${SRC:=/src}"`)
- Working directory is **already `$SRC/{project}`** (no need for `cd $SRC/{project}`)

```bash
#!/bin/bash
set -ex

# Build (only configure on first run)
if [ ! -f Makefile ]; then
    autoreconf -fi  # or ./bootstrap, or skip if not autotools
    ./configure --options
fi
make -j$(nproc)

# Run tests (use -k to continue on failures, || true to not fail script)
make -k check || true

echo "Tests completed"
```

## Examples by Build System

### Autotools Project

```bash
#!/bin/bash
set -ex

if [ ! -f Makefile ]; then
    autoreconf -fi
    ./configure --enable-static
fi
make -j$(nproc)
make -k check || true
```

### CMake Project

```bash
#!/bin/bash
set -ex

if [ ! -f Makefile ]; then
    cmake . -DCMAKE_INSTALL_PREFIX=$WORK -DBUILD_SHARED_LIBS=off
fi
make -j$(nproc)
make test
```

### Project with Dependencies

```bash
#!/bin/bash
set -ex

# Build zlib (only on first run)
if [ ! -f "$WORK/lib/libz.a" ]; then
    pushd "$SRC/zlib"
    ./configure --static --prefix="$WORK"
    make -j$(nproc)
    make install
    popd
fi

# Build main project
if [ ! -f Makefile ]; then
    cmake . -DCMAKE_INSTALL_PREFIX=$WORK
fi
make -j$(nproc)
make test
```

### Project with Submodules (e.g., jq with oniguruma)

```bash
#!/bin/bash
set -ex

# Copy submodule if not present (conditional, not delete-and-copy)
if [ ! -d modules/oniguruma ]; then
    mkdir -p modules
    cp -r $SRC/oniguruma modules/
fi

# Remove failing tests from Makefile.am BEFORE configure
sed -i 's| tests/shtest||g' Makefile.am

if [ ! -f Makefile ]; then
    autoreconf -fi
    ./configure --with-oniguruma=builtin
fi
make -j$(nproc)

# Run tests (some may fail in fuzzing environment)
make -k check || true
```

## Common Errors and Fixes

| Error | Cause | Fix |
|-------|-------|-----|
| `undefined reference to __asan_*` | $WORK shared with ASAN artifacts from build.sh | Use separate prefix like `$SRC/test_deps` in test.sh |
| `Makefile:xxx: No rule to make target` | Partial build state | Use `if [ ! -f Makefile ]` guard around configure |
| `No rule to make target 'tests/shtest'` | Test removed but still in Makefile.am | Use `sed -i 's| tests/shtest||g' Makefile.am` BEFORE configure |
| `source directory already configured` | Previous configure run | Check for Makefile before configure |
| `mv: cannot stat: No such file` | File moved in previous run | Use `cp` with guard |
| `failed to create tests/*.trs` | TESTS= propagates to submodules | Remove failing test files directly |
| `No rule to make target 'jbig.h'` | Original build.sh used `mv` to move files | Rewrite to use `cp` instead |
| `tiff.dict: No such file` | Dict/corpus file doesn't exist | Use conditional copy: `[ -f file ] && cp file dest \|\| true` |
| `No rule to make target 'all'` in submodule | `rm -rf && rsync` deleted submodule's Makefile | Use conditional copy: `if [ ! -d dir ]; then cp; fi` |
| `%destructor redeclaration` / duplicate content | `sed -i` insertion runs every time | Guard with pattern check: `if grep -q 'pattern' && ! grep -q 'new_content'` |
| File grows on each build | `>>` append runs every time | Guard append: `if ! grep -q 'content' file; then ... >> file; fi` |
| `ninja: manifest 'build.ninja' still dirty` | CMake source path mismatch ($SRC changed) | Use `$SRC`-relative build dir instead of `$WORK` |
| `ccache: invalid option -- 'U'` | ccache interferes with Bazel | Remove ccache from PATH: `export PATH=$(echo "$PATH" \| sed 's\|/ccache/bin:\|\|g')` |

## Testing Incremental Builds

**CRITICAL: Use the `test-inc-build` subagent via Task tool to prevent context limit errors.**

The subagent runs tests, analyzes logs internally, and returns only essential summary information.

### Single Project Test

```
Task tool:
  subagent_type: "test-inc-build"
  description: "Test incremental build"
  prompt: "Run incremental build test for {project_name} with oss-fuzz path ../oss-fuzz"
```

### AIXCC Projects: Use Full Path Prefix

**IMPORTANT: For AIXCC projects, use the `aixcc/c/` or `aixcc/jvm/` prefix in the project name.**

Examples:
- `aixcc/c/afc-sqlite3-delta-01`
- `aixcc/c/atlanta-libjpeg-full-01`
- `aixcc/jvm/afc-jenkins-delta-01`

### Parallel Testing Multiple Projects

Use Task tool with `run_in_background: true` in a single message:

```
Task tool (run_in_background: true):
  subagent_type: "test-inc-build"
  prompt: "Run incremental build test for aixcc/c/afc-libexif-delta-01 with oss-fuzz path ../oss-fuzz"

Task tool (run_in_background: true):
  subagent_type: "test-inc-build"
  prompt: "Run incremental build test for aixcc/c/afc-libexif-delta-02 with oss-fuzz path ../oss-fuzz"

Then use TaskOutput to retrieve results from each agent.
```

### What the Test Does

1. Runs build.sh (first run)
2. Runs docker commit to save state
3. Runs build.sh again (incremental run)
4. Compares build times and reports reduction percentage

### Subagent Return Format

**On Success:**
```
## Result: SUCCESS
**Project:** {project_name}
**Build Time Reduction:** {percentage}%
**Summary:** Incremental build working correctly.
```

**On Failure:**
```
## Result: FAILED
**Project:** {project_name}
**Error Type:** {category}
**Error Message:** {exact error, max 10 lines}
**Suggested Fix:** {brief fix}
```

**Success criteria:**
- Both runs complete without errors
- Build time reduction is reported (higher % = better incremental build support)

## Checklist

- [ ] Uses `$SRC` variable, NOT hardcoded paths like `/src` or `/built-src`
- [ ] Uses `if [ ! -f Makefile ]` guard around configure/autoreconf
- [ ] Does NOT delete build artifacts (.o, .lo, .a, .la, .libs)
- [ ] Uses `cp` instead of `mv` where applicable
- [ ] Failing tests removed from Makefile.am BEFORE configure (not just file deletion)
- [ ] Dependencies have existence guards
- [ ] Directory copies are conditional (`if [ ! -d ]`), not delete-and-copy
- [ ] Uses `$SRC` subdirectories instead of `$WORK` for build dirs and install prefixes
- [ ] test.sh uses separate prefix (e.g., `$SRC/test_deps`) if dependencies needed
- [ ] Dict/corpus copies use conditional: `[ -f file ] && cp ... || true`
- [ ] Tests use `make -k check || true` if some may fail
- [ ] Internal build.sh rewritten if it uses `mv` or non-idempotent operations
- [ ] `sed -i` insertions guarded with pattern + content check
- [ ] Append operations (`>>`) guarded with content existence check
- [ ] CMake/Ninja build directories use `$SRC`-relative paths (not `$WORK`)
- [ ] Bazel builds have ccache removed from PATH
- [ ] Works on first run AND repeated runs
