Skip to content

feat/ADFA-3674-template-custom-functions template custom functions#1175

Merged
davidschachterADFA merged 3 commits intostagefrom
feat/ADFA-3674-template-custom-functions
Apr 11, 2026
Merged

feat/ADFA-3674-template-custom-functions template custom functions#1175
davidschachterADFA merged 3 commits intostagefrom
feat/ADFA-3674-template-custom-functions

Conversation

@jomen-adfa
Copy link
Copy Markdown
Contributor

Capability to dynamically load pebble template extension custom functions from the CGT

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

📝 Walkthrough
  • Features

    • Dynamic Pebble template extension loading: templates can now include an embedded extensions.jar (META_EXTENSION_JAR = "extensions.jar") that is discovered and loaded at recipe execution time and its io.pebbletemplates.pebble.extension.Extension implementations registered with the Pebble engine.
    • Context threading for recipe execution: TemplateRecipeExecutor now requires an Android Context injected via its constructor and TemplateDetailsFragment passes requireContext().applicationContext into recipe execution to enable file-system and cache access during template execution.
    • Extension discovery implementation: ZipRecipeExecutor uses DexClassLoader and ServiceLoader to discover and instantiate Pebble Extension implementations from the extracted JAR, registering them on PebbleEngine.Builder before building the engine.
    • New constants: META_EXTENSION_JAR ("extensions.jar") and DEX_OPT_FOLDER ("dex_opt") added to ZipTemplateConstants.
  • Technical changes

    • TemplateDetailsFragment: passes applicationContext into TemplateRecipeExecutor when invoking template.recipe.execute(...)
    • TemplateRecipeExecutor: class signature changed to require override val context: Context and implements RecipeExecutor methods using application assets and file operations.
    • RecipeExecutor interface: added optional val context: Context? get() = null to allow implementations to expose a Context when available (backwards-compatible default null).
    • ZipRecipeExecutor: refactored to optionally extract extensions.jar to a temp file under context.codeCacheDir, create a DexClassLoader with an optimized-dex dir under Environment.TEMPLATES_DIR/DEX_OPT_FOLDER/, load implementations via ServiceLoader, and register discovered Extension instances with the Pebble engine. Loads fail gracefully with logging when extraction/classloading/ServiceLoader iteration fails or when executor.context is unavailable.
  • Risks & Best-practices concerns

    • Dynamic code loading security: loading and instantiating classes from template-provided JARs via DexClassLoader executes arbitrary code inside the app process — templates must be treated as untrusted unless explicitly verified. Consider signature or checksum verification, capability restrictions, or other sandboxing measures.
    • Resource and lifecycle management: extracted JARs and optimized dex output are written to context.codeCacheDir and Environment.TEMPLATES_DIR/DEX_OPT_FOLDER; ensure cleanup and disk usage controls to prevent cache bloat.
    • Permission and concurrency: writing to cache and templates directories requires careful permission handling and coordination to avoid race conditions in concurrent or multi-process scenarios.
    • Silent/fail-gracefully behavior: extension loading failures are logged but suppressed, which can obscure template issues; consider surfacing diagnostics or telemetry for troubleshooting.
    • Classloader leaks and memory: dynamic classloading can increase memory usage and risk retention of classloaders/references; review lifecycle, unloading strategy, and potential leaks.
    • Compatibility and validation: no explicit version or API compatibility checks for loaded extensions; extensions may be incompatible with the app's Pebble version and cause runtime errors.
    • Testing and complexity: the extension-loading path adds significant complexity and must be covered with unit/integration tests for success and failure cases (malformed JARs, permission errors, ServiceLoader edge cases).

Walkthrough

Recipe execution now carries an Android Context through RecipeExecutor -> TemplateRecipeExecutor; ZipRecipeExecutor uses that context to extract a template extensions.jar, load it via DexClassLoader, discover Pebble Extensions with ServiceLoader, and register them into PebbleEngine before rendering templates.

Changes

Cohort / File(s) Summary
API & Executor
templates-api/src/main/java/com/itsaky/androidide/templates/RecipeExecutor.kt, app/src/main/java/com/itsaky/androidide/utils/TemplateRecipeExecutor.kt
Added nullable val context: android.content.Context? to RecipeExecutor. TemplateRecipeExecutor now requires a Context in its constructor and overrides the context property.
UI call site
app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt
Passes requireContext().applicationContext into TemplateRecipeExecutor when executing a template recipe.
Zip template constants
templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt
Added META_EXTENSION_JAR = "extensions.jar" and DEX_OPT_FOLDER = "dex_opt".
Extension discovery & engine build
templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt
If extensions.jar exists and executor.context is present, extracts JAR to context.codeCacheDir, attempts permission fixes, creates per-template optimized dex output dir, constructs a DexClassLoader, uses ServiceLoader on that class loader to find io.pebbletemplates.pebble.extension.Extension implementations, and registers them with PebbleEngine.Builder. Failures return empty list and are logged.

Sequence Diagram

sequenceDiagram
    participant Fragment as TemplateDetailsFragment
    participant Exec as TemplateRecipeExecutor
    participant ZipExec as ZipRecipeExecutor
    participant Archive as Template Archive (zip)
    participant Cache as CodeCacheDir
    participant DexLoader as DexClassLoader
    participant Service as ServiceLoader
    participant Pebble as PebbleEngine

    Fragment->>Exec: instantiate with applicationContext
    Fragment->>Exec: exec.template.recipe.execute(exec)
    Exec->>ZipExec: execute(recipe, executor)
    ZipExec->>Archive: open archive, look for META_EXTENSION_JAR
    Archive-->>ZipExec: extensions.jar (if present)
    ZipExec->>Cache: write temp extensions.jar to codeCacheDir
    ZipExec->>DexLoader: create DexClassLoader (jar, optimized dex dir)
    ZipExec->>Service: ServiceLoader.load(Extension, DexClassLoader)
    Service-->>ZipExec: Extension instances
    loop register each Extension
        ZipExec->>Pebble: builder.extension(ext)
    end
    Pebble-->>ZipExec: built engine with extensions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • Daniel-ADFA
  • itsaky-adfa

Poem

🐰 I dug a hole in code so deep and neat,
Found jars that let Pebble sprout new feet.
Dex hops, ServiceLoader sniffs around,
Extensions bloom where templates are found.
Hooray — projects grow from this small feat! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat/ADFA-3674-template-custom-functions template custom functions' accurately describes the main change: adding capability to dynamically load Pebble template extension custom functions.
Description check ✅ Passed The description 'Capability to dynamically load pebble template extension custom functions from the CGT' directly relates to the changeset, describing the key feature being implemented across the modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ADFA-3674-template-custom-functions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt`:
- Around line 108-109: The background lambda passed to executeAsyncProvideError
captures requireContext() directly which can throw IllegalStateException if the
fragment detaches and also improperly passes a fragment Context to background
work; fix it by capturing val appCtx = requireContext().applicationContext on
the UI thread before calling executeAsyncProvideError and then construct
TemplateRecipeExecutor(appCtx) inside the lambda so template.recipe.execute(...)
uses the application context instead of requireContext().

In
`@templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt`:
- Around line 81-87: The code force-unwraps executor.context when calling
loadExtensionFromArchive; change this to guard against null: check if
executor.context is null before using it (e.g., if executor.context == null then
log or warn and skip processing extensionsEntry) and only call
loadExtensionFromArchive(extensionsEntry, executor.context) when context is
non-null, then iterate and call builder.extension(ext) as before; this avoids
the NullPointerException while preserving behavior (or alternatively throw a
descriptive IllegalStateException if you prefer failing fast).
- Around line 352-364: The loadExtensionFromArchive function currently calls
zipProvider() and reopens the archive (and uses a ZipEntry from a different
ZipFile), causing a resource leak and unsafe mixing of ZipEntry instances;
change loadExtensionFromArchive signature to accept the already-open ZipFile
(the instance created and used in execute) alongside the ZipEntry and Context,
then use that passed ZipFile to obtain the InputStream (e.g.,
zipFile.getInputStream(entry)) instead of calling zipProvider(), removing any
new ZipFile creation and ensuring the original execute-managed use block remains
responsible for closing the archive.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b37dde92-16e9-4ae4-97f9-5952bb62976c

📥 Commits

Reviewing files that changed from the base of the PR and between d45ac54 and a44db17.

📒 Files selected for processing (5)
  • app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt
  • app/src/main/java/com/itsaky/androidide/utils/TemplateRecipeExecutor.kt
  • templates-api/src/main/java/com/itsaky/androidide/templates/RecipeExecutor.kt
  • templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt
  • templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt

Comment thread app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt`:
- Around line 427-430: Fix the typo in the exception message inside
ZipRecipeExecutor.kt's catch block: update the Exception creation in the catch
(e: Throwable) handler where error("ServiceLoader iteration failed for
${entry.name}", Exception("FServiceLoader iteration failed", e)) is called so
the inner Exception message reads "ServiceLoader iteration failed" (replace
"FServiceLoader" with "ServiceLoader") to ensure consistent and correct logging.
- Around line 363-374: The temp JAR created in ZipRecipeExecutor.kt (tempJar) is
never deleted, causing accumulation in context.codeCacheDir; after you
construct/use the DexClassLoader (and after any loading/initialization that
depends on that JAR and its optimized dex in optimizedDir), explicitly delete
tempJar (e.g., tempJar.delete() and/or tempJar.deleteOnExit()) and log any
failure; ensure deletion happens in a finally block or after all uses so the
file is removed even when exceptions are thrown around zip.getInputStream(entry)
or when creating/using the DexClassLoader.
- Around line 314-317: The private method resolveBoolean in ZipRecipeExecutor
(ZipRecipeExecutor.kt) is unused dead code; delete the entire
resolveBoolean(Boolean?, Boolean): ResolvedParam<Boolean> method declaration
from the class to remove the dead method and then rebuild/compile to ensure no
callers remain and no unused-import warnings are introduced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b807ef13-1033-4c80-9be5-eda5943f6078

📥 Commits

Reviewing files that changed from the base of the PR and between a44db17 and fff1694.

📒 Files selected for processing (2)
  • app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt
  • templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt (1)

363-363: Add deleteOnExit() to mitigate disk accumulation over time.

Per the prior discussion about DexClassLoader's lazy JAR references, immediate deletion isn't viable. However, the acceptable mitigation of tempJar.deleteOnExit() mentioned in that conversation hasn't been implemented. This will clean up temp JARs when the process terminates, preventing accumulation across sessions.

Suggested fix
         val tempJar = File.createTempFile("ext_", ".jar", context.codeCacheDir)
+        tempJar.deleteOnExit()

Based on learnings: "Acceptable mitigations include tempJar.deleteOnExit() or storing the JAR at a deterministic path (e.g., alongside the optimized DEX directory) for cleanup on the next run."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt`
at line 363, The temporary JAR created via File.createTempFile("ext_", ".jar",
context.codeCacheDir) in ZipRecipeExecutor (tempJar variable) needs to call
tempJar.deleteOnExit() immediately after creation to avoid accumulating stale
temp files across runs; update the code where tempJar is instantiated to invoke
deleteOnExit() on that File instance (or alternatively store at a deterministic
path) so the JVM will remove the file when the process exits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt`:
- Line 363: The temporary JAR created via File.createTempFile("ext_", ".jar",
context.codeCacheDir) in ZipRecipeExecutor (tempJar variable) needs to call
tempJar.deleteOnExit() immediately after creation to avoid accumulating stale
temp files across runs; update the code where tempJar is instantiated to invoke
deleteOnExit() on that File instance (or alternatively store at a deterministic
path) so the JVM will remove the file when the process exits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f3bc119-9ed9-4326-afa1-84acb7909d04

📥 Commits

Reviewing files that changed from the base of the PR and between fff1694 and 4de0436.

📒 Files selected for processing (1)
  • templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt

@davidschachterADFA davidschachterADFA merged commit a6531a4 into stage Apr 11, 2026
2 checks passed
@davidschachterADFA davidschachterADFA deleted the feat/ADFA-3674-template-custom-functions branch April 11, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants