11package com.itsaky.androidide.templates.impl.zip
22
3- import com.itsaky.androidide.templates.Language
3+ import android.annotation.SuppressLint
4+ import android.content.Context
5+ import dalvik.system.DexClassLoader
6+
47import java.io.File
58import java.io.StringWriter
69import java.util.zip.ZipFile
10+ import java.util.ServiceLoader
711
8- import org.slf4j.LoggerFactory
912import io.pebbletemplates.pebble.PebbleEngine
1013import io.pebbletemplates.pebble.loader.StringLoader
1114import io.pebbletemplates.pebble.lexer.Syntax
15+ import io.pebbletemplates.pebble.error.PebbleException
16+ import io.pebbletemplates.pebble.extension.Extension
1217
18+ import com.itsaky.androidide.templates.Language
1319import com.itsaky.androidide.templates.ModuleTemplateData
1420import com.itsaky.androidide.templates.Parameter
1521import com.itsaky.androidide.templates.ProjectTemplateData
@@ -18,11 +24,12 @@ import com.itsaky.androidide.templates.RecipeExecutor
1824import com.itsaky.androidide.templates.TemplateRecipe
1925import com.itsaky.androidide.templates.impl.base.ProjectTemplateRecipeResultImpl
2026import com.itsaky.androidide.utils.Environment
21- import io.pebbletemplates.pebble.error.PebbleException
2227
28+ import org.slf4j.LoggerFactory
2329import org.adfa.constants.ANDROID_GRADLE_PLUGIN_VERSION
2430import org.adfa.constants.KOTLIN_VERSION
2531import org.adfa.constants.Sdk
32+ import java.util.zip.ZipEntry
2633
2734class ZipRecipeExecutor (
2835 private val zipProvider : () -> ZipFile ,
@@ -69,12 +76,27 @@ class ZipRecipeExecutor(
6976 .setCommentCloseDelimiter(DELIM_COMMENT_CLOSE )
7077 .build()
7178
72- val pebbleEngine = PebbleEngine .Builder ()
79+ val builder = PebbleEngine .Builder ()
80+
81+ val extensionsEntry = zip.getEntry(META_EXTENSION_JAR )
82+ if (extensionsEntry != null ) {
83+ val context = executor.context
84+ if (context == null ) {
85+ warn(" Skipping $META_EXTENSION_JAR because TemplateRecipeExecutor.context is unavailable" )
86+ } else {
87+ val extensions = loadExtensionFromArchive(zip, extensionsEntry, context)
88+ for (ext in extensions) {
89+ builder.extension(ext)
90+ }
91+ }
92+ }
93+
94+ val pebbleEngine = builder.loader(StringLoader ())
7395 .strictVariables(true )
74- .loader(StringLoader ())
7596 .syntax(customSyntax)
7697 .build()
7798
99+
78100 val className = data.name.replace(CLASS_NAME_PATTERN , " " )
79101 val (baseIdentifiers, warnings) = metaJson.pebbleParams(data, defModule, params)
80102 val identifiers = baseIdentifiers + (KEY_CLASS_NAME to className)
@@ -289,10 +311,10 @@ class ZipRecipeExecutor(
289311 else ResolvedParam (value, false )
290312 }
291313
292- private fun resolveBoolean (raw : Boolean? , default : Boolean ): ResolvedParam <Boolean > {
314+ private fun resolveBoolean (raw : Boolean? , default : Boolean ): ResolvedParam <Boolean > {
293315 return if (raw == null ) ResolvedParam (default, true )
294316 else ResolvedParam (raw, false )
295- }
317+ }
296318
297319 private fun filterAndNormalizeZipEntry (
298320 entryName : String ,
@@ -330,4 +352,83 @@ class ZipRecipeExecutor(
330352
331353 private fun Exception.wrap (msg : String ): RuntimeException =
332354 RuntimeException (msg, this )
355+
356+ @SuppressLint(" SetWorldReadable" )
357+ private fun loadExtensionFromArchive (
358+ zip : ZipFile ,
359+ entry : ZipEntry ,
360+ context : Context ,
361+ ): List <Extension > {
362+
363+ val tempJar = File .createTempFile(" ext_" , " .jar" , context.codeCacheDir)
364+
365+ try {
366+ zip.getInputStream(entry).use { input ->
367+ tempJar.outputStream().use { output ->
368+ input.copyTo(output)
369+ }
370+ }
371+ } catch (e: Exception ) {
372+ error(" Failed to extract ${entry.name} to ${tempJar.absolutePath} " , e)
373+ return emptyList()
374+ }
375+
376+ try {
377+ tempJar.setReadable(true , false )
378+ tempJar.setWritable(false )
379+ tempJar.setExecutable(false )
380+ } catch (e: SecurityException ) {
381+ warn(" Could not adjust permissions on ${tempJar.absolutePath} $e " )
382+ }
383+
384+ val optimizedDir = File (
385+ Environment .TEMPLATES_DIR ,
386+ " $DEX_OPT_FOLDER /${basePath} "
387+ )
388+
389+ if (! optimizedDir.exists() && ! optimizedDir.mkdirs()) {
390+ error(" Failed to create optimized dex directory: ${optimizedDir.absolutePath} " ,
391+ IllegalStateException (" mkdirs() failed for ${optimizedDir.absolutePath} " ))
392+ return emptyList()
393+ }
394+
395+ val classLoader = try {
396+ DexClassLoader (
397+ tempJar.absolutePath,
398+ optimizedDir.absolutePath,
399+ null ,
400+ context.classLoader
401+ )
402+ } catch (e: Exception ) {
403+ error(" Failed to create DexClassLoader for ${entry.name} " , e)
404+ return emptyList()
405+ }
406+
407+ val serviceLoader = try {
408+ ServiceLoader .load(Extension ::class .java, classLoader)
409+ } catch (e: Throwable ) {
410+ error(" ServiceLoader failed for ${entry.name} " ,
411+ Exception (" ServiceLoader failed" , e))
412+ return emptyList()
413+ }
414+
415+ val extensions = mutableListOf<Extension >()
416+
417+ try {
418+ for (ext in serviceLoader) {
419+ try {
420+ log.debug(" Loading ${ext::class .java.name} " )
421+ extensions + = ext
422+ } catch (e: Throwable ) {
423+ error(" Failed to instantiate extension from ${entry.name} " ,
424+ Exception (" Failed to instantiate extension" , e))
425+ }
426+ }
427+ } catch (e: Throwable ) {
428+ error(" ServiceLoader iteration failed for ${entry.name} " ,
429+ Exception (" ServiceLoader iteration failed" , e))
430+ }
431+
432+ return extensions
433+ }
333434}
0 commit comments