Skip to content

Commit a6531a4

Browse files
authored
feat/ADFA-3674-template-custom-functions template custom functions (#1175)
* feat/ADFA-3674-template-custom-functions Template custom extension functions * fixes for potential issues * fix typo
1 parent d45ac54 commit a6531a4

5 files changed

Lines changed: 120 additions & 9 deletions

File tree

app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,9 @@ class TemplateDetailsFragment :
105105
}
106106

107107
viewModel.creatingProject.value = true
108+
val appContext = requireContext().applicationContext
108109
executeAsyncProvideError({
109-
template.recipe.execute(TemplateRecipeExecutor())
110+
template.recipe.execute(TemplateRecipeExecutor(appContext))
110111
}) { result, err ->
111112

112113
viewModel.creatingProject.value = false

app/src/main/java/com/itsaky/androidide/utils/TemplateRecipeExecutor.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.itsaky.androidide.utils
1919

20+
import android.content.Context
2021
import org.adfa.constants.LOCAL_MAVEN_CACHES_DEST
2122
import org.adfa.constants.LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME
2223
import com.blankj.utilcode.util.ResourceUtils
@@ -33,7 +34,9 @@ import java.io.InputStream
3334
*
3435
* @author Akash Yadav
3536
*/
36-
class TemplateRecipeExecutor : RecipeExecutor {
37+
class TemplateRecipeExecutor (
38+
override val context: Context
39+
) : RecipeExecutor {
3740

3841
private val application: IDEApplication
3942
get() = IDEApplication.instance

templates-api/src/main/java/com/itsaky/androidide/templates/RecipeExecutor.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.itsaky.androidide.templates
1919

20+
import android.content.Context
2021
import java.io.File
2122
import java.io.InputStream
2223

@@ -27,6 +28,8 @@ import java.io.InputStream
2728
*/
2829
interface RecipeExecutor {
2930

31+
val context: Context? get() = null
32+
3033
/**
3134
* Get the project template data. This is available only while creating modules in an existing project.
3235
*

templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package 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+
47
import java.io.File
58
import java.io.StringWriter
69
import java.util.zip.ZipFile
10+
import java.util.ServiceLoader
711

8-
import org.slf4j.LoggerFactory
912
import io.pebbletemplates.pebble.PebbleEngine
1013
import io.pebbletemplates.pebble.loader.StringLoader
1114
import 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
1319
import com.itsaky.androidide.templates.ModuleTemplateData
1420
import com.itsaky.androidide.templates.Parameter
1521
import com.itsaky.androidide.templates.ProjectTemplateData
@@ -18,11 +24,12 @@ import com.itsaky.androidide.templates.RecipeExecutor
1824
import com.itsaky.androidide.templates.TemplateRecipe
1925
import com.itsaky.androidide.templates.impl.base.ProjectTemplateRecipeResultImpl
2026
import com.itsaky.androidide.utils.Environment
21-
import io.pebbletemplates.pebble.error.PebbleException
2227

28+
import org.slf4j.LoggerFactory
2329
import org.adfa.constants.ANDROID_GRADLE_PLUGIN_VERSION
2430
import org.adfa.constants.KOTLIN_VERSION
2531
import org.adfa.constants.Sdk
32+
import java.util.zip.ZipEntry
2633

2734
class 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
}

templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ const val ARCHIVE_JSON = "templates.json"
55
const val META_FOLDER = "template"
66
const val META_JSON = "template.json"
77
const val META_THUMBNAIL = "thumb.png"
8+
const val META_EXTENSION_JAR = "extensions.jar"
9+
10+
const val DEX_OPT_FOLDER = "dex_opt"
811

912
const val TEMPLATE_EXTENSION = ".peb"
1013

0 commit comments

Comments
 (0)