Compare commits

...

20 Commits

Author SHA1 Message Date
312f573e94 chore: bump dependencies 2025-11-16 01:25:57 +08:00
902320bcc7 Bump version to 1.0.1 2025-11-16 01:15:00 +08:00
0dd0559a42 refactor: separate escaping logic for JavaPoet and KotlinPoet code generation 2025-11-16 00:36:23 +08:00
a6f6ede46a docs: update quick-start 2025-11-16 00:15:36 +08:00
c4e78707d6 feat: add manifest placeholders support in AndroidGenerateConfig 2025-11-16 00:10:36 +08:00
8d16f24098 refactor: add unsafe ext name in ExtensionAware 2025-11-15 23:05:54 +08:00
10f34148a7 refactor: reorganize import statements in BuildscriptDeployer 2025-11-15 23:05:21 +08:00
c5c23a0794 fix: allow blank names in GropifyConfig validation 2025-11-15 22:05:57 +08:00
00dfe1db93 docs: update quick-start 2025-11-15 21:52:21 +08:00
22e9266ce0 refactor: move package "internal" to "debug" and improve logging throughout the codebase 2025-11-15 21:52:00 +08:00
08885d5d32 docs: update README 2025-11-15 16:14:59 +08:00
e0109b0afc refactor: update Javadoc and replace toPoetNoEscape with toPoetGenerationContent 2025-11-14 17:22:21 +08:00
bb56b66e86 fix: correct finalValue assignment logic in PropertyType for autoConversion 2025-11-14 17:22:01 +08:00
23c7283286 docs: update quick-start 2025-11-14 15:35:15 +08:00
d1626c7884 feat: add expected type class feature in ValueRule for keyValuesRules 2025-11-14 15:35:01 +08:00
a884be697c refactor: replace Any type with PropertyTypeValue in generator and deployer classes 2025-11-13 20:59:34 +08:00
3552786ae3 feat: enhance debug logging in buildscript generation process 2025-11-13 13:22:24 +08:00
24709d1993 refactor: update Logger usage to improve clarity and consistency 2025-11-13 13:22:12 +08:00
d98f101945 fix: improve numeric type determination logic in Generator 2025-11-13 12:02:46 +08:00
c12d9abdac fix: update replacementKeyValues to use correct property 2025-11-13 11:07:49 +08:00
30 changed files with 623 additions and 181 deletions

View File

@@ -1,7 +1,7 @@
# Gropify
[![GitHub license](https://img.shields.io/github/license/HighCapable/Gropify?color=blue&style=flat-square)](https://github.com/HighCapable/Gropify/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/discussion%20dev-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable_Dev)
[![Telegram](https://img.shields.io/badge/discussion-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable_Dev)
[![QQ](https://img.shields.io/badge/discussion-QQ-blue.svg?logo=tencent-qq&logoColor=red&style=flat-square)](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf)
<img src="img-src/icon.svg" width = "100" height = "100" alt="LOGO"/>

View File

@@ -1,7 +1,7 @@
# Gropify
[![GitHub license](https://img.shields.io/github/license/HighCapable/Gropify?color=blue&style=flat-square)](https://github.com/HighCapable/Gropify/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/discussion%20dev-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable_Dev)
[![Telegram](https://img.shields.io/badge/discussion-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable_Dev)
[![QQ](https://img.shields.io/badge/discussion-QQ-blue.svg?logo=tencent-qq&logoColor=red&style=flat-square)](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf)
<img src="img-src/icon.svg" width = "100" height = "100" alt="LOGO"/>

View File

@@ -16,6 +16,19 @@ Time zone of version release date: **UTC+8**
:::
### 1.0.0 | 2025.11.11 &ensp;<Badge type="tip" text="latest" vertical="middle" />
### 1.0.1 | 2025.11.16 &ensp;<Badge type="tip" text="latest" vertical="middle" />
- Fixed the issue where `permanentKeyValues` was incorrectly configured to `replacementKeyValues`
- Optimized the automatic type conversion function for property key-values, fixed the problem of negative long integers being converted to integers
- Optimized log output function, added tags and text colors for each type of log
- Added `keyValueRules` to manually specify the type of property key-values, effective when `useTypeAutoConversion` is enabled
- Enhanced debugging function, added detailed log output in debug mode
- Fixed the issue where `extensionName` was judged as empty and illegal under default settings
- Added judgment content for default extension method names that may cause conflicts
- Added `manifestPlaceholders` property key-value synchronization function to `android` configuration method block
- Fixed the problem of Javapoet and Kotlinpoet processing special escape characters in source code generation
- Fixed other issues that may cause build script compilation failure
### 1.0.0 | 2025.11.11 &ensp;<Badge type="warning" text="stale" vertical="middle" />
- The first version is submitted to Maven

View File

@@ -74,8 +74,19 @@ If you don't want to use Kotlin DSL entirely, you can also migrate only `setting
```kotlin
gropify {
// Enable Gropify, setting it to `false` will disable all features
// Enable Gropify, setting it to `false` will disable all features.
isEnabled = true
// Whether to enable debug mode.
//
// You can help us identify the problem by enabling this option
// to print more debugging information in the logs.
//
// - Note: THIS IS ONLY FOR DEBUGGING!
// The debug log will contain your local environment,
// which may contain sensitive information.
// Please be sure to protect this information.
debugMode = false
}
```
@@ -299,7 +310,12 @@ common {
// These key-values rules are applied when properties' keys exist.
keyValuesRules(
"some.key1" to ValueRule { if (it.contains("_")) it.replace("_", "-") else it },
"some.key2" to ValueRule { "$it-value" }
"some.key2" to ValueRule { "$it-value" },
// You can also specify the expected type class,
// the type you specify will be used during generation,
// and an exception will be thrown if the type cannot be converted correctly.
// If the [useTypeAutoConversion] is not enabled, this parameter will be ignored.
"some.key3" to ValueRule(Int::class)
)
// Set where to find properties' key-values.
@@ -427,6 +443,12 @@ android {
// - Note: If you disable this option, please make sure that there are no other projects
// that also use or not only Gropify to generate code to avoid conflicts.
isIsolationEnabled = true
// Whether to use manifest placeholders' generation.
//
// Disabled by default, when enabled will synchronize the properties' key-values
// to the `manifestPlaceholders` in the `android` configuration method block.
manifestPlaceholders = false
}
```
@@ -622,6 +644,7 @@ In Android projects, many repetitive and fixed properties usually need to be con
```properties
project.namespace=com.highcapable.gropifydemo
project.appName=Gropify Demo
project.compileSdk=36
project.targetSdk=36
project.minSdk=26
@@ -645,6 +668,24 @@ android {
}
```
When you set `manifestPlaceholders = true`, Gropify will automatically synchronize these properties' key-values to `manifestPlaceholders` in the android configuration method block.
You can then use these placeholders directly in `AndroidManifest.xml`.
> The following example
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="${project.appName}"
android:icon="@mipmap/ic_launcher">
...
</application>
</manifest>
```
You no longer need to use `buildConfigField` to add code to `BuildConfig`. With the property key-value code generated by `Gropify`, you can manage your project more flexibly.
You can also use interpolation `${...}` in property key-values to reference each other's content, but recursive references are not allowed.

View File

@@ -8,6 +8,19 @@
:::
### 1.0.0 | 2025.11.11 &ensp;<Badge type="tip" text="最新" vertical="middle" />
### 1.0.1 | 2025.11.16 &ensp;<Badge type="tip" text="最新" vertical="middle" />
- 修复 `permanentKeyValues` 被错误地配置到 `replacementKeyValues` 的问题
- 优化属性键值的类型自动转换功能,修复负数长整型被转换为整型的问题
- 优化日志输出功能,加入每种日志的标签和文字颜色
- 新增 `keyValueRules` 可手动指定属性键值的类型,在启用了 `useTypeAutoConversion` 时生效
- 增强调试功能,加入了调试模式下的详细日志输出
- 修复 `extensionName` 在默认设置情况下被判断为空格式非法的问题
- 增加了可能造成冲突的默认扩展方法名称判断内容
- `android` 配置方法块新增 `manifestPlaceholders` 属性键值同步功能
- 修复源代码生成 Javapoet 和 Kotlinpoet 处理特殊转义字符的问题
- 修复其它可能导致构建脚本编译失败的问题
### 1.0.0 | 2025.11.11 &ensp;<Badge type="warning" text="过旧" vertical="middle" />
- 首个版本提交至 Maven

View File

@@ -76,6 +76,14 @@ plugins {
gropify {
// 启用 Gropify设置为 `false` 将禁用所有功能
isEnabled = true
// 是否启用调试模式
//
// 你可以通过启用此选项在日志中打印更多调试信息帮助我们定位问题
//
// - 注意: 此功能仅用于调试!
// 调试日志将包含你的本地环境,其中可能包含敏感信息,请务必保护好这些信息
debugMode = false
}
```
@@ -283,7 +291,11 @@ common {
// 当属性键存在时应用这些键值规则
keyValuesRules(
"some.key1" to ValueRule { if (it.contains("_")) it.replace("_", "-") else it },
"some.key2" to ValueRule { "$it-value" }
"some.key2" to ValueRule { "$it-value" },
// 你还可以指定期望的类型类,生成时将使用你指定的类型,
// 如果类型无法正确转换,将抛出异常
// 如果未启用 [useTypeAutoConversion],此参数将被忽略
"some.key3" to ValueRule(Int::class)
)
// 设置查找属性键值的位置
@@ -406,6 +418,11 @@ android {
// - 注意: 如果你禁用此选项,请确保没有其他同样使用或不仅使用
// Gropify 生成代码的项目,以避免冲突
isIsolationEnabled = true
// 是否使用清单占位符的生成。
//
// 默认禁用,启用后将同步属性的键值到 `android` 配置方法块中的 `manifestPlaceholders`
manifestPlaceholders = false
}
```
@@ -593,6 +610,7 @@ var version = GropifyDemoProperties.PROJECT_VERSION;
```properties
project.namespace=com.highcapable.gropifydemo
project.appName=Gropify Demo
project.compileSdk=36
project.targetSdk=36
project.minSdk=26
@@ -616,6 +634,24 @@ android {
}
```
当你设置了 `manifestPlaceholders = true` 时,`Gropify` 将自动将这些属性键值同步到 `android` 配置方法块中的 `manifestPlaceholders`。
此时你可以直接在 `AndroidManifest.xml` 中使用这些占位符。
> 示例如下
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="${project.appName}"
android:icon="@mipmap/ic_launcher">
...
</application>
</manifest>
```
你可以无需再使用 `buildConfigField` 向 `BuildConfig` 添加代码,有了 `Gropify` 生成的属性键值代码,你可以更加灵活地管理你的项目。
你还可以在属性键值中使用插值 `${...}` 互相引用其中的内容,但不允许递归引用。

View File

@@ -3,7 +3,7 @@ project.name=Gropify
project.url=https://github.com/HighCapable/Gropify
project.groupName=com.highcapable.gropify
project.moduleName=gropify
project.version=1.0.0
project.version=1.0.1
# Gradle Plugin Configuration
gradle.plugin.moduleName=${project.groupName}.gradle.plugin
gradle.plugin.implementationClass=${project.groupName}.plugin.GropifyPlugin

View File

@@ -21,7 +21,7 @@
*/
@file:Suppress("UnusedReceiverParameter")
package com.highcapable.gropify.internal
package com.highcapable.gropify.debug
import com.highcapable.gropify.plugin.Gropify
import kotlin.contracts.ExperimentalContracts

View File

@@ -21,31 +21,32 @@
*/
@file:Suppress("unused", "LoggingStringTemplateAsArgument")
package com.highcapable.gropify.internal
package com.highcapable.gropify.debug
import com.highcapable.gropify.plugin.Gropify
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import kotlin.properties.Delegates
/**
* Gropify logger.
*/
internal object Logger {
private var logger by Delegates.notNull<Logger>()
/** Debug mode flag. */
var debugMode = false
/**
* Initialize logger with project.
* @param project the project.
* @return [Logger]
*/
fun init(project: Project) = apply {
logger = project.logger
fun debug(msg: Any) {
if (!debugMode) return
println("\u001B[36m[${Gropify.TAG}][DEBUG] $msg\u001B[0m")
}
internal fun debug(msg: Any) = logger.debug("[${Gropify.TAG}] $msg")
internal fun info(msg: Any) = logger.info("[${Gropify.TAG}] $msg")
internal fun warn(msg: Any) = logger.warn("[${Gropify.TAG}] $msg")
internal fun error(msg: Any) = logger.error("[${Gropify.TAG}] $msg")
fun info(msg: Any) {
println("${Gropify.TAG}][INFO] $msg")
}
fun warn(msg: Any) {
println("\u001B[33m[${Gropify.TAG}][WARN] $msg\u001B[0m")
}
fun error(msg: Any) {
println("\u001B[31m[${Gropify.TAG}][ERROR] $msg\u001B[0m")
}
}

View File

@@ -21,8 +21,8 @@
*/
package com.highcapable.gropify.gradle.api.entity
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.gradle.api.extension.getFullName
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.plugin.Gropify
import org.gradle.api.Project
import org.gradle.api.initialization.Settings

View File

@@ -21,7 +21,7 @@
*/
package com.highcapable.gropify.gradle.api.extension
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.utils.extension.camelcase
import com.highcapable.kavaref.extension.classOf
@@ -126,7 +126,14 @@ internal fun Any.asExtension() = this as? ExtensionAware? ?: Gropify.error("This
* @receiver [String]
* @return [Boolean]
*/
internal fun String.isUnSafeExtName() = camelcase().let { it == "ext" || it == "extra" || it == "extraProperties" || it == "extensions" }
internal fun String.isUnSafeExtName() = camelcase().let {
it == "ext" ||
it == "extra" ||
it == "extraProperties" ||
it == "extensions" ||
it == "libs" ||
it == "versionCatalogs"
}
/**
* Since Gradle has an [ExtensionAware] extension,

View File

@@ -21,13 +21,17 @@
*/
package com.highcapable.gropify.plugin
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.require
import com.highcapable.gropify.gradle.api.entity.ProjectDescriptor
import com.highcapable.gropify.internal.require
import com.highcapable.gropify.plugin.config.proxy.GropifyConfig
import com.highcapable.gropify.plugin.config.type.GropifyLocation
import com.highcapable.gropify.plugin.deployer.BuildscriptDeployer
import com.highcapable.gropify.plugin.deployer.SourceCodeDeployer
import com.highcapable.gropify.plugin.generator.extension.PropertyMap
import com.highcapable.gropify.plugin.generator.extension.PropertyTypeValue
import com.highcapable.gropify.plugin.generator.extension.createTypeValue
import com.highcapable.gropify.plugin.generator.extension.createTypeValueByType
import com.highcapable.gropify.utils.extension.hasInterpolation
import com.highcapable.gropify.utils.extension.removeSurroundingQuotes
import com.highcapable.gropify.utils.extension.replaceInterpolation
@@ -66,10 +70,11 @@ internal object DefaultDeployer {
*/
fun init(settings: Settings, config: GropifyConfig) {
DefaultDeployer.config = config
if (!config.isEnabled) return
if (!isEnabled()) return
checkingConfigModified(settings)
Logger.debug("Initializing deployers, config modified: $configModified")
deployers.forEach { it.init(settings, configModified) }
}
@@ -78,8 +83,9 @@ internal object DefaultDeployer {
* @param rootProject the current root project.
*/
fun resolve(rootProject: Project) {
if (!config.isEnabled) return
if (!isEnabled()) return
Logger.debug("Resolving deployers, config modified: $configModified")
deployers.forEach { it.resolve(rootProject, configModified) }
}
@@ -88,8 +94,9 @@ internal object DefaultDeployer {
* @param rootProject the current root project.
*/
fun deploy(rootProject: Project) {
if (!config.isEnabled) return
if (!isEnabled()) return
Logger.debug("Deploying deployers, config modified: $configModified")
deployers.forEach { it.deploy(rootProject, configModified) }
}
@@ -100,17 +107,44 @@ internal object DefaultDeployer {
* @return [PropertyMap]
*/
fun generateMap(config: GropifyConfig.CommonGenerateConfig, descriptor: ProjectDescriptor): PropertyMap {
val properties = mutableMapOf<String, Any>()
val properties = mutableMapOf<String, PropertyTypeValue>()
val resolveProperties = mutableMapOf<Any?, Any?>()
config.permanentKeyValues.forEach { (key, value) -> properties[key] = value }
val locations = mutableMapOf<String, String>()
config.permanentKeyValues.forEach { (key, value) ->
properties[key] = value.createTypeValueByType(config.useTypeAutoConversion, key)
locations[key] = "Permanent Key-Value"
}
config.locations.forEach { location ->
when (location) {
GropifyLocation.CurrentProject -> createProperties(config, descriptor.currentDir).forEach { resolveProperties.putAll(it) }
GropifyLocation.RootProject -> createProperties(config, descriptor.rootDir).forEach { resolveProperties.putAll(it) }
GropifyLocation.Global -> createProperties(config, descriptor.homeDir).forEach { resolveProperties.putAll(it) }
GropifyLocation.System -> resolveProperties.putAll(System.getProperties())
GropifyLocation.SystemEnv -> resolveProperties.putAll(System.getenv())
GropifyLocation.CurrentProject -> createProperties(config, descriptor.currentDir).forEach {
resolveProperties.putAll(it)
it.forEach { (key, _) -> locations[key.toString()] = location.name }
}
GropifyLocation.RootProject -> createProperties(config, descriptor.rootDir).forEach {
resolveProperties.putAll(it)
it.forEach { (key, _) -> locations[key.toString()] = location.name }
}
GropifyLocation.Global -> createProperties(config, descriptor.homeDir).forEach {
resolveProperties.putAll(it)
it.forEach { (key, _) -> locations[key.toString()] = location.name }
}
GropifyLocation.System -> {
val system = System.getProperties()
resolveProperties.putAll(system)
system.forEach { (key, _) -> locations[key.toString()] = location.name }
}
GropifyLocation.SystemEnv -> {
val systemEnv = System.getenv()
resolveProperties.putAll(systemEnv)
systemEnv.forEach { (key, _) -> locations[key] = location.name }
}
}
}
@@ -133,7 +167,7 @@ internal object DefaultDeployer {
}
} ?: true
}.toMutableMap().also { resolveKeyValues ->
resolveKeyValues.onEach { (key, value) ->
resolveKeyValues.forEach { (key, value) ->
val resolveKeys = mutableListOf<String>()
fun String.resolveValue(): String = replaceInterpolation { matchKey ->
@@ -152,15 +186,43 @@ internal object DefaultDeployer {
}
if (value.hasInterpolation()) resolveKeyValues[key] = value.resolveValue()
}.takeIf { config.keyValuesRules.isNotEmpty() }?.forEach { (key, value) ->
config.keyValuesRules[key]?.also { resolveKeyValues[key] = it(value) }
}
properties.putAll(resolveKeyValues)
properties.putAll(resolveKeyValues.map { (key, value) ->
key to value.createTypeValue(config.useTypeAutoConversion, key)
})
}
// Replace all key-values if exists.
config.replacementKeyValues.forEach { (key, value) -> properties[key] = value }
config.replacementKeyValues.forEach { (key, value) ->
properties[key] = value.createTypeValueByType(config.useTypeAutoConversion, key)
locations[key] = "Replacement Key-Value${locations[key]?.let { ", $it" }}"
}
// Apply key-values rules.
properties.forEach { (key, value) ->
val (mapper, type) = config.keyValuesRules[key] ?: return@forEach
val resolveValue = mapper(value.raw).createTypeValue(config.useTypeAutoConversion, key, type)
properties[key] = resolveValue
locations[key] = "Rule-based Key-Value${locations[key]?.let { ", $it" }}"
}
properties.forEach { (key, value) ->
Logger.debug(
"""
Generated property for ${config.name}
----------
[Key]: $key
[Value]: ${value.raw}
[Code Value]: ${value.codeValue}
[Type]: ${value.type.simpleName}
[Location]: ${locations[key] ?: "Unknown"}
----------
""".trimIndent()
)
}
return properties
}
@@ -185,4 +247,9 @@ internal object DefaultDeployer {
lastModifiedHashCode = gradleHashCode
}
}
private fun isEnabled(): Boolean {
if (!config.isEnabled) Logger.debug("Gropify is disabled, skipping deployment process.")
return config.isEnabled
}
}

View File

@@ -21,11 +21,11 @@
*/
package com.highcapable.gropify.plugin
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.gradle.api.GradleDescriptor
import com.highcapable.gropify.gradle.api.extension.getOrCreate
import com.highcapable.gropify.gradle.api.plugin.PluginLifecycle
import com.highcapable.gropify.internal.Logger
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.plugin.extension.dsl.configure.GropifyConfigureExtension
import org.gradle.api.Project
import org.gradle.api.initialization.Settings
@@ -46,15 +46,22 @@ internal class GropifyLifecycle : PluginLifecycle {
override fun onSettingsEvaluated(settings: Settings) {
val config = configure?.build(settings) ?: Gropify.error("Extension \"${GropifyConfigureExtension.NAME}\" create failed.")
Logger.debugMode = config.debugMode
Logger.debug("Gropify ${Gropify.VERSION} running on Gradle ${GradleDescriptor.version}")
Logger.debug("Loaded configuration.")
DefaultDeployer.init(settings, config)
}
override fun beforeProjectEvaluate(rootProject: Project) {
Logger.init(rootProject)
Logger.debug("Before project evaluate: $rootProject")
DefaultDeployer.resolve(rootProject)
}
override fun afterProjectEvaluate(rootProject: Project) {
Logger.debug("After project evaluate: $rootProject")
DefaultDeployer.deploy(rootProject)
}
}

View File

@@ -23,8 +23,8 @@
package com.highcapable.gropify.plugin
import com.highcapable.gropify.internal.Logger
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.error
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.initialization.Settings
@@ -56,7 +56,7 @@ class GropifyPlugin<T : ExtensionAware> internal constructor() : Plugin<T> {
}
}
}
is Project -> Logger.init(target).error(
is Project -> Logger.error(
"Gropify can only applied in settings.gradle or settings.gradle.kts, but current is $target, stop loading.",
)
else -> Gropify.error("Gropify applied to an unknown target: $target, stop loading.")

View File

@@ -21,9 +21,10 @@
*/
package com.highcapable.gropify.plugin.compiler
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.debug.require
import com.highcapable.gropify.gradle.api.entity.Dependency
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.internal.require
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.utils.extension.deleteEmptyRecursively
import com.highcapable.gropify.utils.extension.toFile
@@ -111,7 +112,20 @@ internal object CodeCompiler {
writeMetaInf(outputSourcesDir)
createJar(dependency, outputDir, outputBuildDir, outputClassesDir, outputSourcesDir)
} else Gropify.error("Failed to compile java files into path: $outputDirPath\n$diagnosticsMessage")
} else {
Logger.debug("Compilation process failed, dumping source file content.")
files.forEach {
Logger.debug(
"Check this Java file: ${it.name}\n" +
"====== BEGIN FILE CONTENT ======\n" +
"${it.getCharContent(true)}\n" +
"====== END FILE CONTENT ======"
)
}
Logger.debug("Please report those file content to us, you can remove sensitive information before this.")
Gropify.error("Failed to compile Java files into path: $outputDirPath\n$diagnosticsMessage")
}
}
private fun createJar(dependency: Dependency, outputDir: File, buildDir: File, classesDir: File, sourcesDir: File) {

View File

@@ -64,9 +64,9 @@ internal object DefaultConfig {
?: createCommonGenerateConfig(name).permanentKeyValues
override val replacementKeyValues
get() = selfCommon?.permanentKeyValues
?: globalCommon?.permanentKeyValues
?: createCommonGenerateConfig(name).permanentKeyValues
get() = selfCommon?.replacementKeyValues
?: globalCommon?.replacementKeyValues
?: createCommonGenerateConfig(name).replacementKeyValues
override val excludeKeys
get() = selfCommon?.excludeKeys
@@ -126,6 +126,8 @@ internal object DefaultConfig {
override val isIsolationEnabled get() = true
override val manifestPlaceholders get() = false
override val isEnabled
get() = selfCommon?.isEnabled
?: globalCommon?.isEnabled
@@ -142,9 +144,9 @@ internal object DefaultConfig {
?: createCommonGenerateConfig(name).permanentKeyValues
override val replacementKeyValues
get() = selfCommon?.permanentKeyValues
?: globalCommon?.permanentKeyValues
?: createCommonGenerateConfig(name).permanentKeyValues
get() = selfCommon?.replacementKeyValues
?: globalCommon?.replacementKeyValues
?: createCommonGenerateConfig(name).replacementKeyValues
override val excludeKeys
get() = selfCommon?.excludeKeys
@@ -220,9 +222,9 @@ internal object DefaultConfig {
?: createCommonGenerateConfig(name).permanentKeyValues
override val replacementKeyValues
get() = selfCommon?.permanentKeyValues
?: globalCommon?.permanentKeyValues
?: createCommonGenerateConfig(name).permanentKeyValues
get() = selfCommon?.replacementKeyValues
?: globalCommon?.replacementKeyValues
?: createCommonGenerateConfig(name).replacementKeyValues
override val excludeKeys
get() = selfCommon?.excludeKeys
@@ -296,9 +298,9 @@ internal object DefaultConfig {
?: createCommonGenerateConfig(name).permanentKeyValues
override val replacementKeyValues
get() = selfCommon?.permanentKeyValues
?: globalCommon?.permanentKeyValues
?: createCommonGenerateConfig(name).permanentKeyValues
get() = selfCommon?.replacementKeyValues
?: globalCommon?.replacementKeyValues
?: createCommonGenerateConfig(name).replacementKeyValues
override val excludeKeys
get() = selfCommon?.excludeKeys

View File

@@ -186,6 +186,11 @@ private fun GropifyConfigureExtension.AndroidGenerateConfigureScope.create(
?: global?.isIsolationEnabled
?: DefaultConfig.createAndroidGenerateConfig(name, selfCommon, globalCommon).isIsolationEnabled
override val manifestPlaceholders
get() = this@create.manifestPlaceholders
?: global?.manifestPlaceholders
?: DefaultConfig.createAndroidGenerateConfig(name, selfCommon, globalCommon).manifestPlaceholders
override val existsPropertyFiles
get() = this@create.existsPropertyFiles
?: global?.existsPropertyFiles

View File

@@ -85,7 +85,11 @@ internal interface GropifyConfig {
/**
* Android project generate configuration interface.
*/
interface AndroidGenerateConfig : JvmGenerateConfig
interface AndroidGenerateConfig : JvmGenerateConfig {
/** Whether to use manifest placeholders' generation. */
val manifestPlaceholders: Boolean
}
/**
* Jvm project generate configuration interface.

View File

@@ -21,10 +21,12 @@
*/
package com.highcapable.gropify.plugin.deployer
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.gradle.api.entity.Dependency
import com.highcapable.gropify.gradle.api.entity.ProjectDescriptor
import com.highcapable.gropify.gradle.api.extension.addDependencyToBuildscript
import com.highcapable.gropify.gradle.api.extension.getOrCreate
import com.highcapable.gropify.gradle.api.extension.hasExtension
import com.highcapable.gropify.gradle.api.extension.toClassOrNull
import com.highcapable.gropify.plugin.DefaultDeployer
import com.highcapable.gropify.plugin.Gropify
@@ -90,14 +92,16 @@ internal class BuildscriptDeployer(private val _config: () -> GropifyConfig) : D
}
override fun resolve(rootProject: Project, configModified: Boolean) {
if (!buildscriptAccessorsDir.resolve(buildscriptAccessorsDependency.relativePath).isEmpty())
rootProject.addDependencyToBuildscript(buildscriptAccessorsDir.absolutePath, buildscriptAccessorsDependency)
if (buildscriptAccessorsDir.resolve(buildscriptAccessorsDependency.relativePath).isEmpty()) return
Logger.debug("Resolving classpath for $buildscriptAccessorsDependency")
rootProject.addDependencyToBuildscript(buildscriptAccessorsDir.absolutePath, buildscriptAccessorsDependency)
}
override fun deploy(rootProject: Project, configModified: Boolean) {
fun Project.deploy() {
val config = config.from(this).buildscript
if (!config.isEnabled) return
if (!isEnabled(config)) return
val className = buildscriptGenerator.propertiesClass(config.name)
val accessorsClass = className.toClassOrNull(this) ?: throw RuntimeException(
@@ -108,7 +112,13 @@ internal class BuildscriptDeployer(private val _config: () -> GropifyConfig) : D
""".trimIndent()
)
getOrCreate(config.extensionName.camelcase(), accessorsClass)
val extensionName = config.extensionName.camelcase()
getOrCreate(extensionName, accessorsClass)
if (hasExtension(extensionName))
Logger.debug("Created buildscript extension \"$extensionName\" for $this")
else Logger.warn("Failed to create buildscript extension \"$extensionName\" for $this")
}
rootProject.deploy()
@@ -120,4 +130,9 @@ internal class BuildscriptDeployer(private val _config: () -> GropifyConfig) : D
.resolve(GropifyConfigureExtension.NAME)
.resolve(GropifyConfig.ARTIFACTS_NAME)
.apply { mkdirs() }
private fun isEnabled(config: GropifyConfig.BuildscriptGenerateConfig): Boolean {
if (!config.isEnabled) Logger.debug("Config buildscript is disabled in ${config.name}, skipping deployment process.")
return config.isEnabled
}
}

View File

@@ -21,11 +21,11 @@
*/
package com.highcapable.gropify.plugin.deployer
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.gradle.api.entity.ProjectDescriptor
import com.highcapable.gropify.gradle.api.extension.getFullName
import com.highcapable.gropify.gradle.api.extension.getOrNull
import com.highcapable.gropify.internal.Logger
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.plugin.DefaultDeployer.generateMap
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.plugin.config.extension.from
@@ -85,13 +85,13 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
val sourceCodeType = decideSourceCodeType(config, projectType)
val generateDirPath = resolveGenerateDirPath(config)
if (!config.isEnabled) return
if (!isEnabled(config)) return
val outputDir = file(generateDirPath)
val properties = generateMap(config, ProjectDescriptor.create(project = this))
if (!configModified && properties == cachedProjectProperties[getFullName()] && !outputDir.isEmpty()) {
if (config.isEnabled) configureSourceSets(project = this)
if (config.isEnabled) configureSourceSets(project = this, properties)
return
}
@@ -107,10 +107,10 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
val generateConfig = GenerateConfig(packageName, className)
sourceCodeGenerator.build(config, generateConfig, properties).let { generator ->
sourceCodeGenerator.build(projectType, config, generateConfig, properties).let { generator ->
generator.first { it.type == sourceCodeType }
}.writeTo(outputDir)
configureSourceSets(project = this)
configureSourceSets(project = this, properties)
}
rootProject.generate()
@@ -121,7 +121,7 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
}
}
private fun configureSourceSets(project: Project) {
private fun configureSourceSets(project: Project, properties: PropertyMap) {
val projectType = project.resolveType()
val config = config.from(project).let {
@@ -133,6 +133,8 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
} ?: return
}
Logger.debug("Configuring source sets in project '${project.getFullName()}' (${projectType.name}).")
val sourceCodeType = decideSourceCodeType(config, projectType)
val resolveSourceCodeType = if (projectType == ProjectType.Java) SourceCodeSpec.Type.Java else sourceCodeType
val generateDirPath = resolveGenerateDirPath(config)
@@ -147,7 +149,9 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
ProjectType.Java -> javaExtension
ProjectType.KMP -> kotlinExtension
else -> return
} ?: return
} ?: return Logger.debug("No supportable extension found for configuring source sets in $project")
Logger.debug("Found $extension: ${extension.javaClass}")
val collection = extension.asResolver().optional(!debugMode).firstMethodOrNull {
name = "getSourceSets"
@@ -158,7 +162,7 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
name = "getName"
}?.invokeQuietly<String>() == config.sourceSetName
} ?: return Logger.warn(
"Could not found source sets \"${config.sourceSetName}\" in project '${project.getFullName()}' ($projectType)."
"Could not found source set \"${config.sourceSetName}\" in project '${project.getFullName()}' ($projectType)."
)
val directorySet = sourceSet.asResolver().optional(!debugMode).firstMethodOrNull {
@@ -172,6 +176,8 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
name = "getSrcDirs"
}?.invokeQuietly<Set<*>>()
Logger.debug("Deploying generated source to \"$generateDirPath\".")
val alreadyAdded = srcDirs?.any { it is File && it.canonicalPath.endsWith(generateDirPath) } == true
if (!alreadyAdded) {
val resolver = directorySet?.asResolver()?.optional(!debugMode)?.firstMethodOrNull {
@@ -182,6 +188,24 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
resolver?.invokeQuietly(generateDirPath) ?: Logger.error(
"Project '${project.getFullName()}' source sets deployed failed, method \"srcDir\" maybe failed during the processing."
)
} else Logger.debug("Source directory \"$generateDirPath\" already added to source set \"${config.sourceSetName}\", skipping.")
if (projectType == ProjectType.Android &&
config is GropifyConfig.AndroidGenerateConfig &&
config.manifestPlaceholders
) {
Logger.debug("Deploying to manifestPlaceholders.")
val defaultConfig = extension.asResolver().optional(!debugMode).firstMethodOrNull {
name = "getDefaultConfig"
emptyParameters()
}?.invokeQuietly()
defaultConfig?.asResolver()?.optional(!debugMode)?.firstMethodOrNull {
name = "addManifestPlaceholders"
parameters(Map::class)
superclass()
}?.invokeQuietly(properties.map { (key, value) -> key to value.raw }.toMap())
}
}
@@ -213,4 +237,16 @@ internal class SourceCodeDeployer(private val _config: () -> GropifyConfig) : De
return "${className.upperCamelcase()}Properties"
}
private fun isEnabled(config: GropifyConfig.SourceCodeGenerateConfig): Boolean {
if (!config.isEnabled) Logger.debug("Config ${
when (config){
is GropifyConfig.AndroidGenerateConfig -> "android"
is GropifyConfig.JvmGenerateConfig -> "jvm"
is GropifyConfig.KmpGenerateConfig -> "kmp"
else -> "unknown"
}
} is disabled in ${config.name}, skipping deployment process.")
return config.isEnabled
}
}

View File

@@ -23,18 +23,20 @@
package com.highcapable.gropify.plugin.extension.dsl.configure
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.debug.require
import com.highcapable.gropify.gradle.api.extension.isUnSafeExtName
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.internal.require
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.plugin.config.extension.create
import com.highcapable.gropify.plugin.config.proxy.GropifyConfig
import com.highcapable.gropify.plugin.config.type.GropifyLocation
import com.highcapable.gropify.plugin.generator.extension.PropertyValueMapper
import com.highcapable.gropify.plugin.generator.extension.PropertyValueRule
import com.highcapable.gropify.utils.KeywordsDetector
import com.highcapable.gropify.utils.extension.isStartsWithLetter
import org.gradle.api.Action
import org.gradle.api.initialization.Settings
import kotlin.reflect.KClass
/**
* Configure extension for Gropify.
@@ -65,6 +67,10 @@ open class GropifyConfigureExtension internal constructor() {
*
* You can help us identify the problem by enabling this option
* to print more debugging information in the logs.
*
* - Note: THIS IS ONLY FOR DEBUGGING!
* The debug log will contain your local environment,
* which may contain sensitive information. Please be sure to protect this information.
*/
var debugMode = false
@JvmName("debugMode") set
@@ -107,6 +113,17 @@ open class GropifyConfigureExtension internal constructor() {
)
val isEnabled: Boolean get() = Gropify.error("No getter available.")
/**
* Please call it from top level [GropifyConfigureExtension].
* @throws IllegalStateException
*/
@Suppress("unused")
@Deprecated(
message = "Please call it from top level `GropifyConfigureExtension`.",
level = DeprecationLevel.ERROR
)
val debugMode: Boolean get() = Gropify.error("No getter available.")
/**
* Configure common.
*
@@ -156,7 +173,17 @@ open class GropifyConfigureExtension internal constructor() {
@JvmName("extensionName") set
}
open inner class AndroidGenerateConfigureScope internal constructor() : JvmGenerateConfigureScope()
open inner class AndroidGenerateConfigureScope internal constructor() : JvmGenerateConfigureScope() {
/**
* Whether to use manifest placeholders' generation.
*
* Disabled by default, when enabled will synchronize the properties' key-values
* to the `manifestPlaceholders` in the `android` configuration method block.
*/
var manifestPlaceholders: Boolean? = null
@JvmName("manifestPlaceholders") set
}
open inner class JvmGenerateConfigureScope internal constructor() : CommonCodeGenerateConfigureScope() {
@@ -447,8 +474,13 @@ open class GropifyConfigureExtension internal constructor() {
*
* ```kotlin
* keyValuesRules(
* "some.key1" to createValueRule { if (it.contains("_")) it.replace("_", "-") else it },
* "some.key2" to createValueRule { "$it-value" }
* "some.key1" to ValueRule { if (it.contains("_")) it.replace("_", "-") else it },
* "some.key2" to ValueRule { "$it-value" },
* // You can also specify the expected type class,
* // the type you specify will be used during generation,
* // and an exception will be thrown if the type cannot be converted correctly.
* // If the [useTypeAutoConversion] is not enabled, this parameter will be ignored.
* "some.key3" to ValueRule(Int::class)
* )
* ```
*
@@ -483,10 +515,13 @@ open class GropifyConfigureExtension internal constructor() {
/**
* Create a new properties' values rule.
* @param type specify the expected type class, or null to auto-detect,
* if the [useTypeAutoConversion] is not enabled, this parameter will be ignored.
* @param rule callback current rule.
* @return [PropertyValueRule]
*/
fun ValueRule(rule: PropertyValueRule) = rule
@JvmOverloads
fun ValueRule(type: KClass<*>? = null, rule: PropertyValueMapper = { it }) = rule to type
/**
* Set where to find properties' key-values.
@@ -521,7 +556,7 @@ open class GropifyConfigureExtension internal constructor() {
*/
internal fun build(settings: Settings): GropifyConfig {
fun String.checkingStartWithLetter(description: String) {
Gropify.require(isStartsWithLetter()) {
Gropify.require(isBlank() || isStartsWithLetter()) {
"$description name \"$this\" must start with a letter."
}
}

View File

@@ -21,16 +21,16 @@
*/
package com.highcapable.gropify.plugin.generator
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.debug.require
import com.highcapable.gropify.gradle.api.GradleDescriptor
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.internal.require
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.plugin.config.proxy.GropifyConfig
import com.highcapable.gropify.plugin.extension.accessors.proxy.ExtensionAccessors
import com.highcapable.gropify.plugin.generator.extension.PropertyMap
import com.highcapable.gropify.plugin.generator.extension.createTypedValue
import com.highcapable.gropify.plugin.generator.extension.PropertyTypeValue
import com.highcapable.gropify.plugin.generator.extension.toOptimize
import com.highcapable.gropify.plugin.generator.extension.toPoetNoEscape
import com.highcapable.gropify.utils.extension.capitalize
import com.highcapable.gropify.utils.extension.firstNumberToLetter
import com.highcapable.gropify.utils.extension.uncapitalize
@@ -42,7 +42,6 @@ import com.palantir.javapoet.JavaFile
import com.palantir.javapoet.MethodSpec
import com.palantir.javapoet.TypeSpec
import javax.lang.model.element.Modifier
import kotlin.properties.Delegates
/**
* Generator for buildscript accessors classes.
@@ -69,8 +68,6 @@ internal class BuildscriptGenerator {
}
}
private var config by Delegates.notNull<GropifyConfig.BuildscriptGenerateConfig>()
private val classSpecs = mutableMapOf<String, TypeSpec.Builder>()
private val constructorSpecs = mutableMapOf<String, MethodSpec.Builder>()
private val preAddConstructorSpecNames = mutableListOf<Pair<String, String>>()
@@ -97,7 +94,7 @@ internal class BuildscriptGenerator {
private fun createClassSpec(name: String, accessorsName: String = "", isInner: Boolean = true) =
TypeSpec.classBuilder(if (isInner) name.capitalized() else name).apply {
if (isInner) {
addJavadoc("The \"$accessorsName\" accessors.")
addJavadoc("The \$S accessors.", accessorsName)
addSuperinterface(classOf<ExtensionAccessors>())
addModifiers(Modifier.PUBLIC, Modifier.STATIC)
@@ -119,14 +116,14 @@ internal class BuildscriptGenerator {
private fun TypeSpec.Builder.addSuccessiveField(accessorsName: String, className: String) = addField(
FieldSpec.builder(className.capitalized().asClassType(), className.uncapitalized(), Modifier.PRIVATE, Modifier.FINAL).apply {
addJavadoc("Create the \"$accessorsName\" accessors.")
addJavadoc("Create the \$S accessors.", accessorsName)
}.build()
)
private fun TypeSpec.Builder.addSuccessiveMethod(accessorsName: String, methodName: String, className: String) =
addMethod(
MethodSpec.methodBuilder("get${getOrCreateUsedSuccessiveMethodName(methodName, className).capitalize()}").apply {
addJavadoc("Resolve the \"$accessorsName\" accessors.")
addJavadoc("Resolve the \$S accessors.", accessorsName)
addModifiers(Modifier.PUBLIC, Modifier.FINAL)
returns(className.capitalized().asClassType())
@@ -134,16 +131,15 @@ internal class BuildscriptGenerator {
}.build()
)
private fun TypeSpec.Builder.addFinalValueMethod(accessorsName: String, methodName: String, className: String, value: Any) =
private fun TypeSpec.Builder.addFinalValueMethod(accessorsName: String, methodName: String, className: String, value: PropertyTypeValue) =
addMethod(
MethodSpec.methodBuilder("get${getOrCreateUsedSuccessiveMethodName(methodName, className).capitalize()}").apply {
val typedValue = value.createTypedValue(config.useTypeAutoConversion)
val safeValueForJavadoc = typedValue.second.replace("$", "$$")
val safeValueForJavadoc = value.codeValue.replace("$", "$$")
addJavadoc("Resolve the \"$accessorsName\" value \"${value.toString().toPoetNoEscape()}\".")
addJavadoc("Resolve the \$S value \$S.", accessorsName, value.raw)
addModifiers(Modifier.PUBLIC, Modifier.FINAL)
returns(typedValue.first.java)
returns(value.type.java)
addStatement("return $safeValueForJavadoc")
}.build()
)
@@ -175,7 +171,7 @@ internal class BuildscriptGenerator {
*
* After the parsing is completed, we need to call [releaseParseTypeSpec] to complete the parsing.
*/
private fun parseTypeSpec(successiveName: String, key: String, value: Any) {
private fun parseTypeSpec(successiveName: String, key: String, value: PropertyTypeValue) {
fun String.duplicateGrandSuccessiveIndex() = lowercase().let { name ->
if (grandSuccessiveDuplicateIndexes.contains(name)) {
grandSuccessiveDuplicateIndexes[name] = (grandSuccessiveDuplicateIndexes[name] ?: 1) + 1
@@ -273,8 +269,6 @@ internal class BuildscriptGenerator {
"Class name cannot be empty or blank."
}
this.config = config
val topClassName = "${config.name.replace(":", "_").upperCamelcase()}$TOP_CLASS_SUFFIX_NAME"
memoryExtensionClasses[config.name] = "$ACCESSORS_PACKAGE_NAME.$topClassName"
@@ -307,7 +301,10 @@ internal class BuildscriptGenerator {
}
val files = mutableListOf<JavaFile>()
if (allConfig.isEmpty()) return@runCatching files
if (allConfig.isEmpty()) return@runCatching let {
Logger.debug("No buildscript accessors classes to generate.")
files
}
clearGeneratedData(clearAll = true)
allConfig.forEachIndexed { index, configs ->
@@ -324,8 +321,10 @@ internal class BuildscriptGenerator {
files.add(buildTypeSpec().createJavaFile(ACCESSORS_PACKAGE_NAME))
}
Logger.debug("Generated buildscript accessors classes: [${files.joinToString { it.typeSpec().name() }}]")
files
}.getOrElse { Gropify.error("Failed to generated accessors classes.\n$it") }
}.getOrElse { Gropify.error("Failed to generated accessors classes.\n${it.stackTraceToString()}") }
/**
* Generate compile only stub files for buildscript accessors.

View File

@@ -21,16 +21,14 @@
*/
package com.highcapable.gropify.plugin.generator
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.plugin.config.proxy.GropifyConfig
import com.highcapable.gropify.plugin.generator.config.GenerateConfig
import com.highcapable.gropify.plugin.generator.config.SourceCodeSpec
import com.highcapable.gropify.plugin.generator.extension.PropertyMap
import com.highcapable.gropify.plugin.generator.extension.createTypedValue
import com.highcapable.gropify.plugin.generator.extension.escapeForJavaPoet
import com.highcapable.gropify.plugin.generator.extension.toOptimize
import com.highcapable.gropify.plugin.generator.extension.toPoetNoEscape
import com.highcapable.gropify.plugin.generator.extension.toPoetSpace
import com.highcapable.gropify.plugin.generator.extension.toUnderscores
import com.highcapable.gropify.utils.extension.firstNumberToLetter
import com.palantir.javapoet.ClassName
@@ -62,17 +60,18 @@ internal class JavaCodeGenerator {
if (!config.isRestrictedAccessEnabled) addModifiers(Modifier.PUBLIC)
keyValues.toOptimize().toUnderscores().forEach { (key, value) ->
val typedValue = value.second.createTypedValue(config.useTypeAutoConversion)
val currentKey = value.first
val currentValue = value.second
addField(
FieldSpec.builder(typedValue.first.java, key.firstNumberToLetter()).apply {
addJavadoc("Resolve the \"${value.first.toPoetNoEscape()}\" value \"${value.second.toString().toPoetNoEscape()}\".")
FieldSpec.builder(currentValue.type.java, key.firstNumberToLetter()).apply {
addJavadoc("Resolve the \$S value \$S.", currentKey, currentValue.raw)
if (!config.isRestrictedAccessEnabled)
addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
else addModifiers(Modifier.STATIC, Modifier.FINAL)
initializer(typedValue.second.toPoetNoEscape().toPoetSpace())
initializer(currentValue.codeValue.escapeForJavaPoet())
}.build()
)
}
@@ -88,5 +87,5 @@ internal class JavaCodeGenerator {
)
}.build()
SourceCodeSpec(SourceCodeSpec.Type.Java, javaFile)
}.getOrElse { Gropify.error("Failed to generated Java file.\n$it") }
}.getOrElse { Gropify.error("Failed to generated Java file.\n${it.stackTraceToString()}") }
}

View File

@@ -21,16 +21,14 @@
*/
package com.highcapable.gropify.plugin.generator
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.plugin.config.proxy.GropifyConfig
import com.highcapable.gropify.plugin.generator.config.GenerateConfig
import com.highcapable.gropify.plugin.generator.config.SourceCodeSpec
import com.highcapable.gropify.plugin.generator.extension.PropertyMap
import com.highcapable.gropify.plugin.generator.extension.createTypedValue
import com.highcapable.gropify.plugin.generator.extension.escapeForKotlinPoet
import com.highcapable.gropify.plugin.generator.extension.toOptimize
import com.highcapable.gropify.plugin.generator.extension.toPoetNoEscape
import com.highcapable.gropify.plugin.generator.extension.toPoetSpace
import com.highcapable.gropify.plugin.generator.extension.toUnderscores
import com.highcapable.gropify.utils.extension.firstNumberToLetter
import com.squareup.kotlinpoet.FileSpec
@@ -66,19 +64,20 @@ internal class KotlinCodeGenerator {
if (config.isRestrictedAccessEnabled) addModifiers(KModifier.INTERNAL)
keyValues.toOptimize().toUnderscores().forEach { (key, value) ->
val typedValue = value.second.createTypedValue(config.useTypeAutoConversion)
val currentKey = value.first
val currentValue = value.second
addProperty(PropertySpec.builder(key.firstNumberToLetter(), typedValue.first).apply {
addKdoc("Resolve the \"${value.first.toPoetNoEscape()}\" value \"${value.second.toString().toPoetNoEscape()}\".")
addProperty(PropertySpec.builder(key.firstNumberToLetter(), currentValue.type).apply {
addKdoc("Resolve the %S value %S.", currentKey, currentValue.raw)
if (config.isRestrictedAccessEnabled) addModifiers(KModifier.INTERNAL)
addModifiers(KModifier.CONST)
initializer(typedValue.second.toPoetNoEscape().toPoetSpace())
initializer(currentValue.codeValue.escapeForKotlinPoet())
}.build())
}
}.build())
}.build()
SourceCodeSpec(SourceCodeSpec.Type.Kotlin, fileSpec)
}.getOrElse { Gropify.error("Failed to generated Kotlin file.\n$it") }
}.getOrElse { Gropify.error("Failed to generated Kotlin file.\n${it.stackTraceToString()}") }
}

View File

@@ -21,9 +21,11 @@
*/
package com.highcapable.gropify.plugin.generator
import com.highcapable.gropify.internal.require
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.debug.require
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.plugin.config.proxy.GropifyConfig
import com.highcapable.gropify.plugin.deployer.extension.ProjectType
import com.highcapable.gropify.plugin.generator.config.GenerateConfig
import com.highcapable.gropify.plugin.generator.config.SourceCodeSpec
import com.highcapable.gropify.plugin.generator.extension.PropertyMap
@@ -38,16 +40,33 @@ internal class SourceCodeGenerator {
/**
* Build source code specs.
* @param projectType the project type.
* @param config the current generate config.
* @param generateConfig the generate config.
* @param keyValues the properties' key-values map.
* @return [List]<[SourceCodeSpec]>
*/
fun build(config: GropifyConfig.SourceCodeGenerateConfig, generateConfig: GenerateConfig, keyValues: PropertyMap): List<SourceCodeSpec> {
fun build(
projectType: ProjectType,
config: GropifyConfig.SourceCodeGenerateConfig,
generateConfig: GenerateConfig,
keyValues: PropertyMap
): List<SourceCodeSpec> {
Gropify.require(config is GropifyConfig.CommonCodeGenerateConfig) {
"Only Android, Jvm, Kotlin Multiplatform project is supported for now."
}
Logger.debug(
"""
Generated source for ${config.name}
----------
[Class]: ${generateConfig.packageName}.${generateConfig.className}
[Project Type]: $projectType
[Source Set]: ${config.sourceSetName}
[Generate Dir]: ${config.generateDirPath}
----------
""".trimIndent()
)
return listOf(
java.build(config, generateConfig, keyValues),
kotlin.build(config, generateConfig, keyValues)

View File

@@ -21,7 +21,7 @@
*/
package com.highcapable.gropify.plugin.generator.config
import com.highcapable.gropify.internal.error
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.plugin.Gropify
import com.palantir.javapoet.JavaFile
import com.squareup.kotlinpoet.FileSpec

View File

@@ -21,57 +21,13 @@
*/
package com.highcapable.gropify.plugin.generator.extension
import com.highcapable.gropify.utils.extension.isNumeric
import com.highcapable.gropify.utils.extension.underscore
import kotlin.reflect.KClass
internal typealias PropertyMap = MutableMap<String, Any>
internal typealias PropertyOptimizeMap = MutableMap<String, Pair<String, Any>>
internal typealias PropertyValueRule = (value: String) -> String
/**
* Create typed value from [Any] value.
* @param autoConversion whether to enable auto conversion.
* @return [Pair]<[KClass], [String]>
*/
internal fun Any.createTypedValue(autoConversion: Boolean): Pair<KClass<*>, String> {
var isStringType = false
val valueString = toString()
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\\", "\\\\")
.let {
if (autoConversion && (it.startsWith("\"") && it.endsWith("\"") || it.startsWith("'") && it.endsWith("'"))) {
isStringType = true
it.drop(1).dropLast(1)
} else it.replace("\"", "\\\"")
}
if (!autoConversion) return String::class to "\"$valueString\""
val trimmed = valueString.trim()
val typeSpec = when {
isStringType -> String::class
trimmed.toBooleanStrictOrNull() != null -> Boolean::class
trimmed.isNumeric() ->
if (!trimmed.contains(".")) {
val longValue = trimmed.toLongOrNull()
when {
longValue == null -> String::class
longValue > Int.MAX_VALUE -> Long::class
else -> Int::class
}
} else Double::class
else -> String::class
}
val finalValue = when (typeSpec) {
String::class -> "\"$valueString\""
Long::class -> if (trimmed.endsWith("L")) trimmed else "${trimmed}L"
else -> trimmed
}
return typeSpec to finalValue
}
internal typealias PropertyMap = MutableMap<String, PropertyTypeValue>
internal typealias PropertyOptimizeMap = MutableMap<String, Pair<String, PropertyTypeValue>>
internal typealias PropertyValueMapper = (value: String) -> String
internal typealias PropertyValueRule = Pair<PropertyValueMapper, KClass<*>?>
/**
* Optimize property keys for code generation.
@@ -114,15 +70,15 @@ internal fun PropertyOptimizeMap.toUnderscores(): PropertyOptimizeMap {
}
/**
* Replace spaces to middle dot for code generation.
* Escape percentage signs for Javapoet code generation.
* @receiver [String]
* @return [String]
*/
internal fun String.toPoetSpace() = replace(" ", "·")
internal fun String.escapeForJavaPoet() = replace("$", "$$")
/**
* Escape percentage signs for code generation.
* Escape percentage signs and replace spaces to middle dot for Kotlinpoet code generation.
* @receiver [String]
* @return [String]
*/
internal fun String.toPoetNoEscape() = replace("%", "%%")
internal fun String.escapeForKotlinPoet() = replace("%", "%%").replace("$", "\\$").replace(" ", "·")

View File

@@ -0,0 +1,174 @@
/*
* Gropify - A type-safe and modern properties plugin for Gradle.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/Gropify
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/11/13.
*/
package com.highcapable.gropify.plugin.generator.extension
import com.highcapable.gropify.debug.error
import com.highcapable.gropify.debug.require
import com.highcapable.gropify.plugin.Gropify
import com.highcapable.gropify.utils.extension.isNumeric
import kotlin.reflect.KClass
/**
* Create [PropertyTypeValue] from [String] value.
* @receiver [String]
* @param autoConversion whether to enable auto conversion.
* @param key the property key name.
* @param type specify the expected type class, or null to auto-detect,
* if the [autoConversion] is not `true`, this parameter will be ignored.
* @return [PropertyTypeValue]
*/
internal fun String.createTypeValue(autoConversion: Boolean, key: String, type: KClass<*>? = null): PropertyTypeValue {
var isStringType = false
val valueString = replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\\", "\\\\")
.let {
if (autoConversion && (it.startsWith("\"") && it.endsWith("\"") || it.startsWith("'") && it.endsWith("'"))) {
isStringType = true
it.drop(1).dropLast(1)
} else it.replace("\"", "\\\"")
}
if (!autoConversion) return PropertyTypeValue(this, "\"$valueString\"", String::class)
val trimmed = valueString.trim()
if (type != null) {
val finalValue = when (type) {
String::class -> "\"$valueString\""
CharSequence::class -> "\"$valueString\""
Char::class -> trimmed.firstOrNull()?.let { "'$it'" }
?: Gropify.error("The \"$key\" value is empty and cannot be converted to Char type.")
Boolean::class -> if (trimmed.toBooleanStrictOrNull() != null)
trimmed
else ((trimmed.toIntOrNull() ?: 0) > 0).toString()
Int::class -> {
val intValue = trimmed.toIntOrNull()
Gropify.require(intValue != null && intValue in Int.MIN_VALUE..Int.MAX_VALUE) {
"The \"$key\" value \"$this\" cannot be converted to Int type."
}
trimmed
}
Long::class -> {
Gropify.require(trimmed.toLongOrNull() != null) {
"The \"$key\" value \"$this\" cannot be converted to Long type."
}
if (trimmed.endsWith("L")) trimmed else "${trimmed}L"
}
Double::class -> {
val doubleValue = trimmed.toDoubleOrNull()
Gropify.require(doubleValue != null && !doubleValue.isInfinite()) {
"The \"$key\" value \"$this\" cannot be converted to Double type."
}
trimmed
}
Float::class -> {
val floatValue = trimmed.toFloatOrNull()
Gropify.require(floatValue != null && !floatValue.isInfinite()) {
"The \"$key\" value \"$this\" cannot be converted to Float type."
}
if (trimmed.endsWith("f") || trimmed.endsWith("F")) trimmed else "${trimmed}f"
}
else -> Gropify.error(
"Unsupported property \"$key\" value type: ${type.qualifiedName}, " +
"only String, CharSequence, Char, Boolean, Int, Long, Float, Double are supported."
)
}
return PropertyTypeValue(this, finalValue, type)
}
val typeSpec = when {
isStringType -> String::class
trimmed.toBooleanStrictOrNull() != null -> Boolean::class
trimmed.isNumeric() ->
if (!trimmed.contains(".")) {
val longValue = trimmed.toLongOrNull()
when (longValue) {
null -> String::class
in Int.MIN_VALUE..Int.MAX_VALUE -> Int::class
else -> Long::class
}
} else {
val doubleValue = trimmed.toDoubleOrNull()
if (doubleValue == null || doubleValue.isInfinite())
String::class
else Double::class
}
else -> String::class
}
val finalValue = when (typeSpec) {
String::class -> "\"$valueString\""
Long::class -> if (trimmed.endsWith("L")) trimmed else "${trimmed}L"
else -> trimmed
}
return PropertyTypeValue(this, finalValue, typeSpec)
}
/**
* Create [PropertyTypeValue] from [Any] value's type.
* @receiver [Any]
* @param autoConversion whether to enable auto conversion.
* @param key the property key name.
* @return [PropertyTypeValue]
*/
internal fun Any.createTypeValueByType(autoConversion: Boolean, key: String): PropertyTypeValue {
val typeSpec = this.javaClass.kotlin
val valueString = toString()
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\\", "\\\\")
.replace("\"", "\\\"")
val trimmed = valueString.trim()
if (!autoConversion) return PropertyTypeValue(this.toString(), "\"$valueString\"", String::class)
val finalValue = when (typeSpec) {
String::class, CharSequence::class -> "\"$trimmed\""
Char::class -> "'$trimmed'"
Boolean::class -> trimmed
Long::class -> if (trimmed.endsWith("L")) trimmed else "${trimmed}L"
Float::class -> if (trimmed.endsWith("f") || trimmed.endsWith("F")) trimmed else "${trimmed}f"
Int::class, Double::class -> trimmed
else -> Gropify.error(
"Unsupported property \"$key\" value type: ${typeSpec.qualifiedName}, " +
"only String, CharSequence, Char, Boolean, Int, Long, Float, Double are supported."
)
}
return PropertyTypeValue(this.toString(), finalValue, typeSpec)
}
/**
* Property type value entity.
*/
internal data class PropertyTypeValue(
val raw: String,
val codeValue: String,
val type: KClass<*>
)

View File

@@ -21,10 +21,10 @@
*/
package com.highcapable.gropify.plugin.helper
import com.highcapable.gropify.debug.Logger
import com.highcapable.gropify.gradle.api.extension.getFullName
import com.highcapable.gropify.gradle.api.extension.getOrNull
import com.highcapable.gropify.gradle.api.extension.hasExtension
import com.highcapable.gropify.internal.Logger
import com.highcapable.gropify.plugin.deployer.extension.ExtensionName
import com.highcapable.gropify.plugin.extension.dsl.configure.GropifyConfigureExtension
import com.highcapable.kavaref.KavaRef.Companion.asResolver

View File

@@ -18,7 +18,7 @@ dependencyResolutionManagement {
}
plugins {
id("com.highcapable.gropify") version "1.0.0"
id("com.highcapable.gropify") version "1.0.1"
}
gropify {