Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ class MainActivity : EdgeToEdgeIDEActivity() {
builder.show()
}

internal fun openProject(root: File, project: RecentProject? = null) {
internal fun openProject(root: File, project: RecentProject? = null, hasTemplateIssues: Boolean = false) {
ProjectManagerImpl.getInstance().projectPath = root.absolutePath
GeneralPreferences.lastOpenedProject = root.absolutePath

Expand All @@ -416,7 +416,7 @@ class MainActivity : EdgeToEdgeIDEActivity() {
}

// Track project open in Firebase Analytics
analyticsManager.trackProjectOpened(root.absolutePath)
analyticsManager.trackProjectOpened(root.absolutePath)

if (isFinishing) {
return
Expand All @@ -425,6 +425,9 @@ class MainActivity : EdgeToEdgeIDEActivity() {
val intent =
Intent(this, EditorActivityKt::class.java).apply {
putExtra("PROJECT_PATH", root.absolutePath)
if (hasTemplateIssues) {
putExtra("HAS_TEMPLATE_ISSUES", true)
}
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {

observeStates()
startServices()

if (intent.getBooleanExtra("HAS_TEMPLATE_ISSUES", false)) {
flashError(getString(string.msg_template_warnings))
}

}

private fun observeStates() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ class TemplateDetailsFragment :
// open the project
(requireActivity() as MainActivity).openProject(
result.data.projectDir,
project = project
project = project,
hasTemplateIssues = result.hasErrorsWarnings
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.idetooltips.TooltipTag.EXIT_TO_MAIN
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.templates.ProjectTemplate
import com.itsaky.androidide.templates.impl.TemplateProviderImpl
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.viewmodel.MainViewModel
import org.slf4j.LoggerFactory

Expand Down Expand Up @@ -115,11 +117,9 @@ class TemplateListFragment :

log.debug("Reloading templates...")

val templates =
ITemplateProvider
.getInstance(reload = true)
.getTemplates()
.filterIsInstance<ProjectTemplate>()
val provider = ITemplateProvider.getInstance(reload = true)
val templates = provider.getTemplates().filterIsInstance<ProjectTemplate>()
val warnings = (provider as? TemplateProviderImpl)?.warnings.orEmpty()

Comment thread
coderabbitai[bot] marked this conversation as resolved.
adapter =
TemplateListAdapter(
Expand All @@ -140,5 +140,9 @@ class TemplateListFragment :
)
binding.list.adapter = adapter
updateSpanCount()
}

if (warnings.isNotEmpty()) {
requireActivity().flashError(warnings.joinToString("\n"))
}
}
}
2 changes: 2 additions & 0 deletions resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,8 @@
<string name="checkbox_delete_source_after_install">Delete installation file after install</string>
<string name="init_failed_with_reason">%1$s: %2$s</string>

<string name="msg_template_warnings">\n\nProject creation finished with warnings/errors. Open IDE Logs for details.</string>
Comment thread
jomen-adfa marked this conversation as resolved.

<!-- Name of the notification channel which is used to show notifications for wireless debugging -->
<string name="notification_channel_adb_pairing">Wireless debugging pairing</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ val data: D
/**
* Result of recipe execution for a [ProjectTemplate].
*/
interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData<ProjectTemplateData>
interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData<ProjectTemplateData> {
val hasErrorsWarnings: Boolean
get() = false
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Result of recipe execution for a [ModuleTemplate].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class TemplateProviderImpl : ITemplateProvider {
}

private val templates = mutableMapOf<String, Template<*>>()
val warnings: MutableList<String> = mutableListOf()

init {
reload()
Expand All @@ -55,14 +56,15 @@ class TemplateProviderImpl : ITemplateProvider {

for (zipFile in list) {
try {
val zipTemplates = ZipTemplateReader.read(zipFile) { json, params, path, data, defModule ->
val zipTemplates = ZipTemplateReader.read(zipFile, warnings) { json, params, path, data, defModule ->
ZipRecipeExecutor({ ZipFile(zipFile) }, json, params, path, data, defModule)
}

for (t in zipTemplates) {
templates[t.templateId] = t
}
} catch (e: Exception) {
warnings.add("Failed to load template archive $zipFile error: ${e.message}")
log.error("Failed to load template from archive: $zipFile", e)
}
}
Expand All @@ -78,6 +80,7 @@ class TemplateProviderImpl : ITemplateProvider {

override fun reload() {
release()
warnings.clear()
initializeTemplates()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ import com.itsaky.androidide.templates.base.ModuleTemplateBuilder
import com.itsaky.androidide.templates.base.ProjectTemplateBuilder

data class ProjectTemplateRecipeResultImpl(
override val data: ProjectTemplateData
override val data: ProjectTemplateData,
override val hasErrorsWarnings: Boolean = false
) : ProjectTemplateRecipeResult

data class ModuleTemplateRecipeResultImpl(override val data: ModuleTemplateData
) : ModuleTemplateRecipeResult


internal fun ProjectTemplateBuilder.recipeResult(): ProjectTemplateRecipeResult {
return ProjectTemplateRecipeResultImpl(data)
return ProjectTemplateRecipeResultImpl(data)
}

internal fun ModuleTemplateBuilder.recipeResult(): ModuleTemplateRecipeResult {
return ModuleTemplateRecipeResultImpl(data)
return ModuleTemplateRecipeResultImpl(data)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.itsaky.androidide.templates.RecipeExecutor
import com.itsaky.androidide.templates.TemplateRecipe
import com.itsaky.androidide.templates.impl.base.ProjectTemplateRecipeResultImpl
import com.itsaky.androidide.utils.Environment
import io.pebbletemplates.pebble.error.PebbleException

import org.adfa.constants.ANDROID_GRADLE_PLUGIN_VERSION
import org.adfa.constants.KOTLIN_VERSION
Expand All @@ -32,6 +33,8 @@ class ZipRecipeExecutor(
private val defModule: ModuleTemplateData,
) : TemplateRecipe<ProjectTemplateRecipeResult> {

var hasErrorsWarnings: Boolean = false

companion object {
private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java)
}
Expand All @@ -40,11 +43,11 @@ class ZipRecipeExecutor(
executor: RecipeExecutor
): ProjectTemplateRecipeResult {

log.debug("executor called!!")
info("Starting project creation for $basePath")

val projectDir = data.projectDir
if (projectDir.exists()) {
return ProjectTemplateRecipeResultImpl(data)
return ProjectTemplateRecipeResultImpl(data, hasErrorsWarnings)
}

val projectRoot = projectDir.canonicalFile
Expand All @@ -56,77 +59,118 @@ class ZipRecipeExecutor(

zipProvider().use { zip ->

val customSyntax = Syntax.Builder()
.setPrintOpenDelimiter(DELIM_PRINT_OPEN)
.setPrintCloseDelimiter(DELIM_PRINT_CLOSE)
.setExecuteOpenDelimiter(DELIM_EXECUTE_OPEN)
.setExecuteCloseDelimiter(DELIM_EXECUTE_CLOSE)
.setCommentOpenDelimiter(DELIM_COMMENT_OPEN)
.setCommentCloseDelimiter(DELIM_COMMENT_CLOSE)
.build()

val pebbleEngine = PebbleEngine.Builder()
.loader(StringLoader())
.syntax(customSyntax)
.build()

val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params)
log.debug("identifiers warnings: ${warnings.joinToString(System.lineSeparator())}")

val packageName =
resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME)

for (entry in zip.entries()) {
if (!entry.name.startsWith("$basePath/")) continue
if (entry.name == "$basePath/") continue
if (entry.name.startsWith("$basePath/$META_FOLDER/")) continue

if ((metaJson.parameters?.optional?.language != null) &&
(data.language != null) &&
shouldSkipFile(
entry.name.removeSuffix(TEMPLATE_EXTENSION),
safeLanguageName(data.language)
)
) continue

val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue

val relativePath = normalized.removePrefix("$basePath/")
.replace(packageName.value, defModule.packageName.replace(".", "/"))

val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile

if (!outFile.toPath().startsWith(projectRoot.toPath())) {
log.warn("Skipping suspicious ZIP entry outside project dir: {}", entry.name)
continue
val customSyntax = Syntax.Builder()
.setPrintOpenDelimiter(DELIM_PRINT_OPEN)
.setPrintCloseDelimiter(DELIM_PRINT_CLOSE)
.setExecuteOpenDelimiter(DELIM_EXECUTE_OPEN)
.setExecuteCloseDelimiter(DELIM_EXECUTE_CLOSE)
.setCommentOpenDelimiter(DELIM_COMMENT_OPEN)
.setCommentCloseDelimiter(DELIM_COMMENT_CLOSE)
.build()

val pebbleEngine = PebbleEngine.Builder()
.loader(StringLoader())
.syntax(customSyntax)
.build()

val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params)
if (warnings.isNotEmpty()) {
warn("Identifier warnings: ${warnings.joinToString(System.lineSeparator())}")
}

if (entry.isDirectory) {
outFile.mkdirs()
} else {
outFile.parentFile?.mkdirs()

if (entry.name.endsWith(TEMPLATE_EXTENSION)) {
log.debug("template processing ${entry.name}")
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
val template = pebbleEngine.getTemplate(content)
val writer = StringWriter()
template.evaluate(writer, identifiers)
outFile.writeText(writer.toString(), Charsets.UTF_8)
} else {
zip.getInputStream(entry).use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
val packageName =
resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME)

for (entry in zip.entries()) {
if (!entry.name.startsWith("$basePath/")) continue
if (entry.name == "$basePath/") continue
if (entry.name.startsWith("$basePath/$META_FOLDER/")) continue

if ((metaJson.parameters?.optional?.language != null) &&
(data.language != null) &&
shouldSkipFile(
entry.name.removeSuffix(TEMPLATE_EXTENSION),
safeLanguageName(data.language)
)
) continue

val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue

val relativePath = normalized.removePrefix("$basePath/")
.replace(packageName.value, defModule.packageName.replace(".", "/"))

val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile

if (!outFile.toPath().startsWith(projectRoot.toPath())) {
warn("Skipping suspicious template entry outside project dir: ${entry.name}")
continue
}

if (entry.isDirectory) {
outFile.mkdirs()
} else {
try {
outFile.parentFile?.mkdirs()

if (entry.name.endsWith(TEMPLATE_EXTENSION)) {
info("Processing template ${entry.name}")
val content = try {
zip.getInputStream(entry).bufferedReader().use { it.readText() }
} catch (e: Exception) {
throw e.wrap("Failed to read template ${entry.name}")
}

val template = try {
pebbleEngine.getTemplate(content)
} catch (e: PebbleException) {
throw e.wrap(
"Pebble parse error in ${entry.name} at line ${e.lineNumber}: ${e.message}"
)
} catch (e: Exception) {
throw e.wrap("Unexpected Pebble parse error in ${entry.name}")
}

val writer = StringWriter()
val rendered = try {
template.evaluate(writer, identifiers)
} catch (e: PebbleException) {
error(
"Pebble evaluation error in ${entry.name} at line ${e.lineNumber}: ${e.message}", e
)
null
} catch (e: Exception) {
error("Unexpected Pebble evaluation error in ${entry.name}", e)
null
}
if (rendered == null) continue

try {
outFile.writeText(writer.toString(), Charsets.UTF_8)
} catch (e: Exception) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
error("Failed writing output file: ${outFile.absolutePath}", e)
}

} else {
try {
zip.getInputStream(entry).use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
}
}
} catch (e: Exception) {
error("Failed copying binary entry: ${entry.name}", e)
}
}
} catch (e: Exception) {
error("Failed to process template entry: ${entry.name}", e)
}
}
}
}

keystore(executor)

return ProjectTemplateRecipeResultImpl(data)
return ProjectTemplateRecipeResultImpl(data, hasErrorsWarnings)
}

private fun keystore(executor: RecipeExecutor) {
Expand Down Expand Up @@ -266,4 +310,20 @@ class ZipRecipeExecutor(
return normalizedParts.joinToString(File.separator)
}

private fun warn(msg: String) {
hasErrorsWarnings = true
log.warn(msg)
}

private fun info(msg: String) {
log.info(msg)
}

private fun error(msg: String, e: Exception) {
hasErrorsWarnings = true
log.error(msg, e)
}

private fun Exception.wrap(msg: String): RuntimeException =
RuntimeException(msg, this)
}
Loading
Loading