Skip to content

Commit c152312

Browse files
committed
Refactor obfuscation tasks and remove restore functionality: streamline the build pipeline by eliminating in-place source mutation and post-merge restore tasks. Introduce new per-variant obfuscation tasks for string resources and assets, ensuring generated outputs are properly registered with AGP. Update documentation to reflect these changes and clarify the new task behaviors.
1 parent 2d87c0d commit c152312

35 files changed

+378
-826
lines changed

CHANGELOG.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,33 @@
44

55
### Added
66

7-
- **Docs**: [Refactor plan checklist](docs/refactor-plan-checklist.md) — seguimiento hecho/pendiente del roadmap del plugin.
7+
- **Docs**: [AGP generated resources](docs/agp-generated-resources.md) — rationale for `addGeneratedSourceDirectory`.
8+
- **AGP integration**: Per-variant obfuscation outputs are registered with `Sources.addGeneratedSourceDirectory` (`res` and `assets`) so merges consume `build/intermediates/stringcare/generated-*` overlays without touching `src/`.
89

9-
### Fixed
10+
### Changed
1011

11-
- **Restore**: Backups under `temp/<module>/src/...` now copy back to `<module>/src/...` (no duplicated module segment). Restore uses binary `copyTo` for assets; restore-resource task no longer clears temp before restore-assets; `stringcareBeforeMergeAssets` runs after `stringcareBeforeMergeResources`. Restore tasks stay non–UP-TO-DATE so Gradle cannot skip them while sources remain obfuscated.
12+
- **Breaking — build pipeline**: Removed in-place source mutation and post-merge restore. Tasks renamed to `stringcareObfuscateStringResources<Variant>` and `stringcareObfuscateAssets<Variant>`. Removed `stringcareAfterMerge*` / temp backup tree / `Restore*Task` / `RestoreFilesUseCase`.
13+
- **Gradle**: Obfuscate tasks are cache-friendly (`@DisableCachingByDefault` removed); proper `@InputFiles` + `@OutputDirectory` boundaries.
14+
- **Runtime behaviour**: Same APK obfuscation semantics; developer checkout and IDE always see plaintext resources in source control.
1215

13-
### Changed
16+
### Changed (historical notes)
1417

1518
- **Architecture**: Domain models under `domain.models` (with `models` typealiases for compatibility); `infrastructure` packages for parsers, Gradle wiring, crypto, filesystem; thin `ObfuscateStringsUseCase` + `ResourceRepository`.
1619
- **XML**: SAX-first `strings.xml` parsing with DOM fallback for nested markup; `XmlAttributes` / `XmlParser` facade.
17-
- **Tasks**: Gradle `Property` inputs on obfuscate/restore/preview tasks; `@DisableCachingByDefault` (sources are mutated in place — not suitable for `@CacheableTask`).
20+
- **Tasks**: Gradle `Property` inputs on obfuscate/preview tasks.
1821
- **AGP**: `compileOnly` for the Android Gradle Plugin dependency (provided at runtime on Android projects).
1922
- **JAR size**: With `compileOnly` AGP, plugin JAR is ~180KB vs previous multi‑MB fat jar (verify locally with `./gradlew :plugin:jar`).
20-
- **Gradle**: `StringCareBuildService` (shared build service) replaces mutable static state on `StringCarePlugin` for paths, temp dir, and variant applicationIds.
23+
- **Gradle**: `StringCareBuildService` (shared build service) replaces mutable static state on `StringCarePlugin` for configuration and variant applicationIds.
2124
- **Dependencies**: Removed Guava and Gson; added `kotlinx-serialization-json` for task JSON list inputs.
2225
- **Execution**: Shell commands use `ProcessBuilder` with a 60s timeout and structured `ExecutionResult` (`Success` / `Failure` / `Timeout`).
2326
- **Native host libs**: SHA-256 verification before `System.load`, retries, optional verbose logging tied to `debug` in tasks.
2427
- **XML / scan**: Faster attribute iteration in `parseXML`; `walkTopDown` skips `build/`, `.gradle/`, `.git/`, `node_modules/`; `mapNotNull` for resource/asset discovery; idempotent `StringCareConfiguration.normalize()`.
2528
- **Tooling**: Detekt + baseline, ktlint (non-failing), JaCoCo hook, Develocity build scan terms in root `settings.gradle.kts`.
26-
- **Backups**: Resource/asset backups use temp-file copy + atomic move when the filesystem supports it.
27-
- **Tests**: `ObfuscationServiceTest` (JNI roundtrip when loaded), `BackupServiceTest`, UTF-16 / malformed XML parser cases; CI runs `jacocoTestCoverageVerification` with a low interim line threshold.
29+
- **Tests**: `ObfuscationServiceTest` (JNI roundtrip when loaded), UTF-16 / malformed XML parser cases; CI runs `jacocoTestCoverageVerification` with a low interim line threshold.
2830

2931
### Breaking changes
3032

33+
- **Task names / pipeline**: Integrations that referenced `stringcareBeforeMerge*` / `stringcareAfterMerge*` or depended on restore-side effects must use `stringcareObfuscateStringResources*` / `stringcareObfuscateAssets*` instead.
3134
- **Internal APIs**: Static mutable state on `StringCarePlugin` (paths, temp folder, variant map) was removed in favor of `StringCareBuildService`. Any build logic or tests reaching into those internals must use task inputs / the registered build service instead.
3235
- **Dependencies on the plugin JAR**: Guava and Gson are no longer bundled; list-style DSL fields are serialized with Kotlin serialization in task properties. Pure-Java consumers of internal packages are unsupported.
3336

MIGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ stringcare {
1717
|-----|-----|
1818
| `StringCarePlugin.absoluteProjectPath` | Resolved per build via `StringCareBuildService.absoluteProjectPath()` (internal) |
1919
| `StringCarePlugin.variantMap` | `StringCareBuildService` variant map (internal) |
20-
| `StringCarePlugin.tempFolder` / `resetFolder()` | Service `getOrCreateTempFolder()` / `resetTempFolder()`; tests use `StringCareSession` + `StringCarePlugin.resetFolder()` |
20+
| `StringCarePlugin.resetFolder()` | Test hook only: clears `StringCareSession` temp root for integration tests (no production temp backup tree) |
2121

2222
## Native libraries
2323

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ StringCare Android provides **compile-time obfuscation** for strings and assets
1313
- [Publishing](publishing.md) — Release workflow, secrets, and local publish (`publishToMavenLocal`).
1414
- [Migration](migration.md) — Upgrading from 4.x to 5.0 (groupId, plugin ID, package, Gradle/AGP).
1515
- [Architecture](architecture.md) — Mono-repo layout, library vs plugin, JNI, and Variant API.
16-
- [Refactor plan checklist](refactor-plan-checklist.md)Estado del roadmap de refactor del plugin (hecho vs pendiente).
16+
- [AGP generated resources](agp-generated-resources.md)Why the plugin uses `addGeneratedSourceDirectory` instead of mutating `src/` or post-merge XML.
1717
- [Contributing](contributing.md) — Release workflow secrets and local publish steps.
1818
- [Troubleshooting](troubleshooting.md) — Submodule, signing, plugin not found, publish job, JNI/NDK.
1919
- [Verify obfuscation](verify-obfuscation.md) — Comandos `gradlew` para comprobar que strings/assets se ofuscan (nativas del host, `syncPluginNativeLibraries`).

docs/agp-generated-resources.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# AGP: generated `res` / `assets` directories
2+
3+
The Android Gradle Plugin does **not** expose a stable public artifact for “merged resources still as XML” before AAPT2 compiles to `.flat` binaries. Transforming post-merge XML is therefore not a supported first-class workflow for third-party plugins.
4+
5+
StringCare obfuscates **before** merge by writing outputs under `build/intermediates/stringcare/generated-*` and registering them with:
6+
7+
- `variant.sources.res.addGeneratedSourceDirectory(task, Task::outputDirectory)`
8+
- `variant.sources.assets.addGeneratedSourceDirectory(task, Task::outputDirectory)`
9+
10+
This is the same extension point used for other generated resource trees and keeps `src/` untouched while remaining compatible with incremental builds and Gradle caching when task inputs/outputs are declared correctly.

docs/architecture.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ The **library** module:
2222
The **plugin** module:
2323

2424
- Implements the Gradle plugin entry point **`StringCarePlugin`** (implements `Plugin<Project>`), creates the **`stringcare`** extension (`StringCareExtension`) and registers tasks.
25-
- Uses the **AGP 8.7 Variant API**: it does **not** use `BuildListener`. It uses `project.plugins.withId("com.android.application")` and then `project.extensions.getByType(AndroidComponentsExtension::class.java)` and **`onVariants`** to register per-variant tasks (e.g. `stringcareBeforeMergeResourcesDebug`, `stringcareAfterMergeResourcesDebug`, and the same for Assets). Each variant gets before/after merge tasks for resources and for assets; the “before” tasks obfuscate, the “after” tasks restore originals from backup.
25+
- Uses the **AGP Variant API** (`ApplicationAndroidComponentsExtension.onVariants`). For each variant it registers:
26+
- **`stringcareObfuscateStringResources&lt;Variant&gt;`** — reads matching string XML from the module tree, obfuscates into `build/intermediates/stringcare/generated-res/&lt;variant&gt;/`, and registers that directory with **`variant.sources.res.addGeneratedSourceDirectory(...)`** so AGP merges it as an overlay (without modifying `src/`).
27+
- **`stringcareObfuscateAssets&lt;Variant&gt;`** — same for assets via **`variant.sources.assets.addGeneratedSourceDirectory(...)`** and `build/intermediates/stringcare/generated-assets/&lt;variant&gt;/`.
2628
- Contains **JNI** (`.dylib`, `.dll`, `.so`) for the **host** (Gradle on macOS, Windows, or Linux). Prebuilts come from **stringcare-jni** `dist/{macos,linux,windows}/`; **`preparePluginNativeLibraries`** (before `processResources`) copies them into `build/generated/stringcare-plugin-natives/` and they are packaged in the JAR. Optional checked-in copies live under `internal/jni/`. macOS: universal `libsignKey.dylib`; Linux/Windows: x64 + arm64. Separate from the app’s **sc-native-lib** in the library module.
2729

2830
## Flow
2931

30-
- **Compile time:** When you build an Android application that applies the plugin, the plugin runs before the merge of resources and assets. It backs up the configured string XML files and asset files, obfuscates them using the signing certificate fingerprint (or mocked fingerprint), and the merge steps see the obfuscated content. After the merge, the plugin restores the originals so the source tree is unchanged. The APK therefore contains obfuscated strings and assets instead of plain text.
32+
- **Compile time:** Obfuscation tasks write **only** to Gradle intermediates under `build/intermediates/stringcare/`. Merge tasks consume those directories together with normal source sets. The APK/AAB therefore contains obfuscated strings and assets; **checked-in sources stay plaintext**.
3133
- **Runtime:** The app loads the **library** and calls `SC.init(context)`. When you call `SC.reveal(id)` or `SC.reveal(value)`, or use `SCTextView`, the library uses the same certificate (from the running app) to derive the key and decrypt the content. Obfuscation and revelation are symmetric with respect to the signing key.

docs/plugin-tasks.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,22 @@ The plugin registers two tasks you can run from the command line or IDE:
99
Example: `./gradlew :app:stringcarePreview`
1010

1111
- **`stringcareTestObfuscate`**
12-
Performs a dry run of the obfuscation logic (backup, obfuscate, then restore) without changing the final build output. Useful to confirm that obfuscation and key derivation work in your environment.
12+
Runs obfuscation into a temporary directory and prints before/after content. **Source files in the module are never modified.**
1313
Example: `./gradlew :app:stringcareTestObfuscate`
1414

1515
Both tasks are registered once per project (not per variant). They use the first variant’s configuration (e.g. debug) for fingerprint and paths when the project is an Android application.
1616

1717
## Variant tasks (internal)
1818

19-
The plugin also registers **per-variant** tasks that hook into the Android build. You do not need to run these yourself; they are wired before and after the merge steps:
19+
The plugin registers **per-variant** tasks that feed **generated** resource and asset directories into the Android merge pipeline via AGP’s **`addGeneratedSourceDirectory`**. You do not need to run these yourself.
2020

21-
- **`stringcareBeforeMergeResources<Variant>`** — Runs before the variant’s merge of resources; obfuscates the string XML files.
22-
- **`stringcareAfterMergeResources<Variant>`** — Runs after merge; restores the original string files from backup.
23-
- **`stringcareBeforeMergeAssets<Variant>`** — Runs before the variant’s asset merge; obfuscates asset files.
24-
- **`stringcareAfterMergeAssets<Variant>`** — Runs after asset merge; restores the original assets.
21+
- **`stringcareObfuscateStringResources<Variant>`** — Writes obfuscated string XML under `build/intermediates/stringcare/generated-res/<variant>/`. Registered as an extra `res` source layer (highest priority overlay).
22+
- **`stringcareObfuscateAssets<Variant>`** — Writes obfuscated assets under `build/intermediates/stringcare/generated-assets/<variant>/`. Registered as an extra `assets` source layer.
2523

26-
`<Variant>` is the variant name with the first letter capitalized (e.g. `Debug`, `Release`, or `ProdDebug` if you use product flavors). The plugin connects these so that the merge tasks see the obfuscated resources/assets, and the originals are restored afterward so the source tree stays unchanged. Dependencies are set so that the “before” task runs before the corresponding merge task, and the “after” task runs after it.
24+
`<Variant>` is the variant name with the first letter capitalized (e.g. `Debug`, `Release`, or `ProdDebug` if you use product flavors).
2725

28-
### Ordering and temp backups
26+
### Behaviour
2927

30-
- **`stringcareBeforeMergeResources<Variant>`** is ordered to run **before** **`stringcareBeforeMergeAssets<Variant>`** (`mustRunAfter`), so string obfuscation and backups are written first; both phases share the same temp root under `StringCareBuildService`.
31-
- **`stringcareAfterMergeResources<Variant>`** restores from backup using paths relative to the module directory (the Gradle project folder): backups live at `temp/<module>/src/...` and are copied back to `<module>/src/...`, not `<module>/<module>/src/...`.
32-
- **`stringcareAfterMergeResources<Variant>`** does **not** delete the temp tree; **`stringcareAfterMergeAssets<Variant>`** runs last and restores assets (re-copy from the same backups is harmless), then clears the temp directory so backups are not left behind.
33-
- Restore tasks use **`outputs.upToDateWhen { false }`** so Gradle does not skip them while sources are still obfuscated from a previous run.
28+
- **Sources are never modified** — Plaintext files stay in `src/main/res` and `src/main/assets` for version control and IDE indexing.
29+
- **Gradle caching** — Obfuscate tasks declare proper inputs (`xmlInputFiles` / `assetInputFiles`) and outputs (`outputDirectory`), so incremental builds and build cache can apply.
30+
- AGP wires `merge*Resources` / asset merge tasks to depend on the obfuscate tasks automatically when generated directories are registered.

docs/verify-obfuscation.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,25 @@ Tareas típicas (por variante; ejemplo **prodDebug**):
4343

4444
| Tarea | Rol |
4545
|-------|-----|
46-
| `stringcareBeforeMergeResourcesProdDebug` | Ofusca `strings.xml` antes del merge de recursos |
47-
| `stringcareAfterMergeResourcesProdDebug` | Restaura strings desde backup temporal |
48-
| `stringcareBeforeMergeAssetsProdDebug` | Ofusca assets (p. ej. `*.json`) |
49-
| `stringcareAfterMergeAssetsProdDebug` | Restaura assets |
46+
| `stringcareObfuscateStringResourcesProdDebug` | Escribe strings ofuscados en `build/intermediates/stringcare/generated-res/prodDebug/` |
47+
| `stringcareObfuscateAssetsProdDebug` | Escribe assets ofuscados en `build/intermediates/stringcare/generated-assets/prodDebug/` |
5048
| `stringcarePreview` | Vista previa / diagnóstico |
51-
| `stringcareTestObfuscate` | Prueba de ofuscación (tests del plugin) |
49+
| `stringcareTestObfuscate` | Prueba de ofuscación en directorio temporal (no modifica fuentes) |
5250

5351
## 4. Comprobar que no se salta la ofuscación
5452

5553
Con salida detallada:
5654

5755
```bash
58-
./gradlew :app:stringcareBeforeMergeResourcesProdDebug --rerun-tasks --info 2>&1 | tee stringcare-verify.log
56+
./gradlew :app:stringcareObfuscateStringResourcesProdDebug --rerun-tasks --info 2>&1 | tee stringcare-verify.log
5957
```
6058

61-
**Esperado si la nativa carga y hay huella de firma:** mensajes del plugin con variante y clave SHA1, backup de recursos, etc. **No** debe aparecer la línea de *Skipping … native library*.
59+
**Esperado si la nativa carga y hay huella de firma:** mensajes del plugin con variante y clave SHA1, listado de recursos, etc. **No** debe aparecer la línea de *Skipping … native library*.
6260

6361
Comprobar también assets:
6462

6563
```bash
66-
./gradlew :app:stringcareBeforeMergeAssetsProdDebug --rerun-tasks --info 2>&1 | tee -a stringcare-verify.log
64+
./gradlew :app:stringcareObfuscateAssetsProdDebug --rerun-tasks --info 2>&1 | tee -a stringcare-verify.log
6765
```
6866

6967
## 5. Build completa
@@ -72,7 +70,7 @@ Comprobar también assets:
7270
./gradlew :app:clean :app:assembleProdDebug --rerun-tasks --info 2>&1 | tee stringcare-assemble.log
7371
```
7472

75-
Durante el merge, los recursos en disco pueden estar ofuscados de forma temporal; al final **`stringcareAfterMerge*`** restaura los fuentes del módulo. La comprobación fiable es el **log** de las tareas `BeforeMerge` (y que exista huella vía `signingReport` o configuración del plugin), no solo el `values.xml` final empaquetado.
73+
Los **fuentes en `src/`** permanecen en texto claro; la ofuscación va a directorios generados bajo `build/intermediates/stringcare/`. La comprobación fiable es el **log** de las tareas `stringcareObfuscate*` (y que exista huella vía `signingReport` o `mockedFingerprint` en el bloque `stringcare { }`).
7674

7775
## 6. Huella de firma (SHA1)
7876

plugin/config/detekt/baseline.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
<ManuallySuppressedIssues></ManuallySuppressedIssues>
44
<CurrentIssues>
55
<ID>TooGenericExceptionCaught:Stark.kt$Stark.Companion$e: Throwable</ID>
6-
<ID>UnusedParameter:VariantApi.kt$extension: StringCareExtension</ID>
76
<ID>UnusedPrivateProperty:PrintUtils.kt$PrintUtils.Companion$private val logger = LoggerFactory.getLogger(StringCarePlugin::class.java)</ID>
8-
<ID>UnusedPrivateProperty:VariantApi.kt$val beforeStrings = tasks.register( "stringcareBeforeMergeResources$variantCapitalized", ObfuscateStringsTask::class.java, ) { it.projectPath = projectPath it.moduleName = moduleName it.variantName = variantName it.applicationId = applicationId it.skip = config.skip it.debug = config.debug it.mockedFingerprint = config.mockedFingerprint it.srcFoldersJson = gson.toJson(config.srcFolders) it.stringFilesJson = gson.toJson(config.stringFiles) it.assetsFilesJson = gson.toJson(config.assetsFiles) }</ID>
97
</CurrentIssues>
108
</SmellBaseline>

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/domain/models/AssetsFile.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ data class AssetsFile(
66
val file: File,
77
val sourceFolder: String,
88
val module: String,
9-
) : Backupable {
10-
override fun backup(tempRoot: String): File {
11-
val cleanPath =
12-
"$tempRoot${File.separator}$module${File.separator}$sourceFolder${file.absolutePath.split(sourceFolder)[1]}"
13-
.replace("${File.separator}${File.separator}", File.separator)
14-
val backupFile = File(cleanPath)
15-
copyToBackupAtomically(file, backupFile)
16-
return backupFile
9+
) {
10+
/**
11+
* Path relative to the `assets/` directory for writing generated assets.
12+
*/
13+
fun relativePathUnderAssets(): String {
14+
val path = file.absolutePath.replace('\\', '/')
15+
val marker = "/assets/"
16+
val idx = path.indexOf(marker)
17+
require(idx >= 0) { "Expected .../assets/... in ${file.absolutePath}" }
18+
return path.substring(idx + marker.length)
1719
}
1820
}

0 commit comments

Comments
 (0)