background
During SDK development, the interface for obtaining the SDK version number is generally exposed. The obtained version number is generally of String type, such as:
// sdk interface interface Sdk { fun getVersion(): String } // sdk caller sdk.getVersion()
The above method can be through gradle Configure the version number in properties, and then in build Read the version number from gradle and generate it to buildconfig In Java, for example:
// gradle.properties VERSION=1.0.0.0 // builde.gradle android { defaultConfig { buildConfigField("String", "SDK_VERSION", "\"$VERSION\"") } } // SdkImple.kt class SdkImpl : Sdk { override fun getVersion(): String { // Return to SDK in BuildConfig_ VERSION return BuildConfig.SDK_VERSION } }
In the above way, you only need to modify gradle The version number in properties is enough
However, the above method has one disadvantage: the version number provided by the SDK is of String type, which is inconvenient for the third party to adapt and develop according to the version number. The third party needs to judge the size of the version number by itself. The author hopes that the SDK itself can expose the interface to judge the size of the version number
programme
Based on the above requirements, the SDK exposed interface for obtaining version number cannot return String type. The SDK interface is modified as follows:
interface Sdk { // Returns a Version object fun getVersion(): Version } class SdkImpl : Sdk { override fun getVersion(): Version { // Return CURRENT in Version return Version.CURRENT } }
The following is the definition of the Version object 1 The following are examples of different version numbers:
class Version internal constructor( private val major: Int, // Major version 1 private val minor: Int, // Minor version 0 private val patch: Int, // Patch version 0 private val extra: Int, // Keep version 0 private val suffix: String?, // Suffix version, such as alpha01 and beta01 ) : Comparable<Version> { private val version = versionOf(major, minor, patch, extra) // Version Verification private fun versionOf(major: Int, minor: Int, patch: Int, extra: Int): Int { require( major in 0..MAX_COMPONENT_VALUE && minor in 0..MAX_COMPONENT_VALUE && patch in 0..MAX_COMPONENT_VALUE && extra in 0..MAX_COMPONENT_VALUE ) { "Version components are out of range: $major.$minor.$patch.$extra" } return major.shl(24) + minor.shl(16) + patch.shl(8) + extra } override fun toString(): String = if (suffix.isNullOrEmpty()) "$major.$minor.$patch.$extra" else "$major.$minor.$patch.$extra-$suffix" override fun equals(other: Any?): Boolean { if (this === other) return true val otherVersion = (other as? Version) ?: return false return this.version == otherVersion.version } override fun hashCode(): Int = version // Version comparison 1 override fun compareTo(other: Version): Int = version - other.version // Version comparison 2 fun isAtLeast(major: Int, minor: Int): Boolean = this.major > major || (this.major == major && this.minor >= minor) // Version comparison 2 fun isAtLeast(major: Int, minor: Int, patch: Int): Boolean = this.major > major || (this.major == major && (this.minor > minor || this.minor == minor && this.patch >= patch)) // Version comparison 2 fun isAtLeast(major: Int, minor: Int, patch: Int, extra: Int): Boolean = this.major > major || (this.major == major && (this.minor > minor || this.minor == minor && (this.patch > patch || this.patch == patch && this.extra >= extra))) companion object { internal const val MAX_COMPONENT_VALUE = 255 // current version @JvmField val CURRENT: Version = VersionCurrentValue.get() } } private object VersionCurrentValue { @JvmStatic fun get(): Version = Version(0, 0, 0, 0, null) // value is written here automatically during build }
When a third party carries out version adaptation development, it is more convenient to operate as follows:
val version = sdk.getVersion() println("version = $version") if (version.isAtLeast(1, 2)) { // Current version is greater than or equal to 1.2.0.0 // do something } else { // The current version is less than 1.2.0.0 // do something }
Is the above scheme more friendly happy:, I don't know if the readers have found out where to modify the version number?
Careful readers may have found that Version Current is the called VersionCurrentValue#get() method. The VersionCurrentValue#get() method will create an instance of the Version object. You only need to modify the VersionCurrentValue#get() method to pass in the Version number. Wait, you need to modify the VersionCurrentValue#get() method every time you publish the Version?
I vaguely feel that something is wrong. If I forget to modify the VersionCurrentValue#get() method at the time of publishing, it's not terrible
"No man is a saint without faults". Let the program help us generate the version number. At the same time, it is compatible with scheme 1: only modify gradle Properties
Modify the VersionCurrentValue#get() method at compile time using KCP
realization
In the first part Application of kotlin KCP - Part II The author recorded the basic steps of building a KCP environment, which will not be repeated here. Interested readers can read the previous article first
The above figure shows the organizational structure of the project. A brief introduction is as follows:
- sample: contains Version and test class
- Version plugin gradle: gradle plugin in kcp
- Version plugin kotlin: kotlin compiler plugin in kcp
The sample module will not be introduced. The following mainly implements the other two modules
build.gradle.kts - project level
Build at the project level gradle. Configuring plug-in dependencies in KTS script
buildscript { // Configure the unique ID of the Kotlin plug-in extra["kotlin_plugin_id"] = "com.guodong.android.version.kcp" } plugins { kotlin("jvm") version "1.5.31" apply false // Configure Gradle publishing plug-in, and you can no longer write META-INF id("com.gradle.plugin-publish") version "0.16.0" apply false // Configure build BuildConfig plug-in id("com.github.gmazzo.buildconfig") version "3.0.3" apply false } allprojects { // Configure Kotlin plug-in version version = "0.0.1" }
version-plugin-gradle
First, configure build gradle. KTS script
build.gradle.kts - module level
plugins { id("java-gradle-plugin") kotlin("jvm") id("com.github.gmazzo.buildconfig") } dependencies { implementation(kotlin("gradle-plugin-api")) } buildConfig { // Configure the package name of BuildConfig packageName("com.guodong.android.version.kcp.plugin.gradle") // Set the unique ID of the Kotlin plug-in buildConfigField("String", "KOTLIN_PLUGIN_ID", "\"${rootProject.extra["kotlin_plugin_id"]}\"") // Set Kotlin plugin GroupId buildConfigField("String", "KOTLIN_PLUGIN_GROUP", "\"com.guodong.android\"") // Set the ArtifactId of the Kotlin plug-in buildConfigField("String", "KOTLIN_PLUGIN_NAME", "\"version-kcp-kotlin-plugin\"") // Set Kotlin plug-in Version buildConfigField("String", "KOTLIN_PLUGIN_VERSION", "\"${project.version}\"") } gradlePlugin { plugins { create("Version") { id = rootProject.extra["kotlin_plugin_id"] as String // `apply plugin: "com.guodong.android.version.kcp"` displayName = "Version Kcp" description = "Version Kcp" implementationClass = "com.guodong.android.version.kcp.gradle.VersionGradlePlugin" // Plug in entry class } } } tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" }
VersionGradlePlugin
Create VersionGradlePlugin to implement the KotlinCompilerPluginSupportPlugin interface
class VersionGradlePlugin : KotlinCompilerPluginSupportPlugin { override fun apply(target: Project): Unit = with(target) { logger.error("Welcome to guodongAndroid-version kcp gradle plugin.") // Configure the Gradle plug-in extension here extensions.create("version", VersionExtension::class.java) } // If applicable, the default is True override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true // Get the unique ID of the Kotlin plug-in override fun getCompilerPluginId(): String = BuildConfig.KOTLIN_PLUGIN_ID // Get Maven coordinate information of Kotlin plug-in override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact( groupId = BuildConfig.KOTLIN_PLUGIN_GROUP, artifactId = BuildConfig.KOTLIN_PLUGIN_NAME, version = BuildConfig.KOTLIN_PLUGIN_VERSION ) // Read the Gradle plug-in extension information and write the SubpluginOption override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> { val project = kotlinCompilation.target.project val extension = project.extensions.getByType(VersionExtension::class.java) return project.provider { listOf( SubpluginOption(key = "version", value = extension.version) ) } } }
Because the version number needs to be imported into Gradle Plugin in external configuration, a VersionExtension needs to be created here:
open class VersionExtension { var version: String = "0.0.0.0" override fun toString(): String { return "VersionExtension(version=$version)" } }
So far, the Gradle plug-in has been written
version-plugin-kotlin
Next, write the Kotlin compiler plug-in. First, configure build gradle. KTS script
build.gradle.kts - module level
plugins { kotlin("jvm") kotlin("kapt") id("com.github.gmazzo.buildconfig") } dependencies { // Rely on Kotlin compiler Library compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") // Rely on Google auto service kapt("com.google.auto.service:auto-service:1.0") compileOnly("com.google.auto.service:auto-service-annotations:1.0") } buildConfig { // Configure the package name of BuildConfig packageName("com.guodong.android.version.kcp.plugin.kotlin") // Set the unique ID of the Kotlin plug-in buildConfigField("String", "KOTLIN_PLUGIN_ID", "\"${rootProject.extra["kotlin_plugin_id"]}\"") } tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" }
VersionCommandLineProcessor
Implement CommandLineProcessor
@AutoService(CommandLineProcessor::class) class VersionCommandLineProcessor : CommandLineProcessor { companion object { // OptionName corresponds to the Key passed in by VersionGradlePlugin#applyToCompilation() private const val OPTION_VERSION = "version" // ConfigurationKey val ARG_VERSION = CompilerConfigurationKey<String>(OPTION_VERSION) } // Configure the unique ID of the Kotlin plug-in override val pluginId: String = BuildConfig.KOTLIN_PLUGIN_ID // Read the 'SubpluginOptions' parameter and write the' CliOption '` override val pluginOptions: Collection<AbstractCliOption> = listOf( CliOption( optionName = OPTION_VERSION, valueDescription = "string", description = "version string", required = true, ) ) // Process' CliOption 'and write' CompilerConfiguration '` override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { when (option.optionName) { OPTION_VERSION -> configuration.put(ARG_VERSION, value) else -> throw IllegalArgumentException("Unexpected config option ${option.optionName}") } } }
VersionComponentRegistrar
Implement ComponentRegistrar
@AutoService(ComponentRegistrar::class) class VersionComponentRegistrar( private val defaultVersion: String, ) : ComponentRegistrar { companion object { internal const val DEFAULT_VERSION = "0.0.0.0" } @Suppress("unused") // Used by service loader constructor() : this(DEFAULT_VERSION) override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { // Get log collector val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) // Get the incoming version number val version = configuration.get(VersionCommandLineProcessor.ARG_VERSION, defaultVersion) // Output the log and check whether it is executed // CompilerMessageSeverity.INFO - no log output seen // CompilerMessageSeverity.ERROR - the compilation process stops executing messageCollector.report(CompilerMessageSeverity.STRONG_WARNING, "Welcome to guodongAndroid-version kcp kotlin plugin") // Register the extension here in 'ClassBuilderInterceptorExtension' ClassBuilderInterceptorExtension.registerExtension( project, VersionClassGenerationInterceptor( messageCollector = messageCollector, // Incoming version number version = version ) ) } }
VersionClassGenerationInterceptor
class VersionClassGenerationInterceptor( private val messageCollector: MessageCollector, private val version: String, ) : ClassBuilderInterceptorExtension { // Intercept ClassBuilderFactory override fun interceptClassBuilderFactory( interceptedFactory: ClassBuilderFactory, bindingContext: BindingContext, diagnostics: DiagnosticSink // Custom ClassBuilderFactory delegate to source ClassBuilderFactory ): ClassBuilderFactory = object : ClassBuilderFactory by interceptedFactory { // Copy newClassBuilder override fun newClassBuilder(origin: JvmDeclarationOrigin): ClassBuilder { // Custom ClassBuilder return VersionClassBuilder( messageCollector = messageCollector, // Incoming version number version = version, // Incoming source ClassBuilder delegate = interceptedFactory.newClassBuilder(origin), ) } } }
VersionClassBuilder
class VersionClassBuilder( private val messageCollector: MessageCollector, private val version: String, private val delegate: ClassBuilder, ) : DelegatingClassBuilder() { companion object { private const val VERSION_NAME = "com/guodong/android/VersionCurrentValue" private const val MIN_COMPONENT_VALUE = 0 private const val MAX_COMPONENT_VALUE = 255 } override fun getDelegate(): ClassBuilder { return delegate } override fun newMethod( origin: JvmDeclarationOrigin, access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>? ): MethodVisitor { val original = super.newMethod(origin, access, name, desc, signature, exceptions) val thisName = delegate.thisName // Verify the fully qualified name of VersionCurrentValue if (thisName != VERSION_NAME) { return original } // Verify whether it is in ` build The version number is set in gradle ' if (version == VersionComponentRegistrar.DEFAULT_VERSION) { messageCollector.report( CompilerMessageSeverity.ERROR, "Missing version, need to set version in build.gradle, like this:\n" + "version {\n" + "\tversion = \"1.0.0.0\"\n" + "}" ) } // Structure version number val (major, minor, patch, extra, suffix) = parseVersion() // Return ASM MethodVisitor return VersionMethodVisitor(Opcodes.ASM9, original, major, minor, patch, extra, suffix) } // Resolution version number is ` Multiple` private fun parseVersion(): Multiple<Int, Int, Int, Int, String?> { if (version.isEmpty()) { throw IllegalArgumentException("Version must not be empty.") } val major: Int val minor: Int val patch: Int val extra: Int val suffix: String? if (version.contains("-")) { val split = version.split("-") if (split.size != 2) { throw IllegalArgumentException("Version components must be only contains one `-`.") } val versions = split[0].split(".") val length = versions.size if (length != 4) { throw IllegalArgumentException("Version components must be four digits, it is [ $version ] now.") } try { major = versions[0].toInt() minor = versions[1].toInt() patch = versions[2].toInt() extra = versions[3].toInt() suffix = split[1] } catch (e: NumberFormatException) { val errMsg = "Version components must consist of numbers." val exception = IllegalArgumentException(errMsg) exception.addSuppressed(e) throw exception } } else { val versions = version.split(".") val length = versions.size if (length != 4) { throw IllegalArgumentException("Version components must be four digits, it is [ $version ] now.") } try { major = versions[0].toInt() minor = versions[1].toInt() patch = versions[2].toInt() extra = versions[3].toInt() suffix = null } catch (e: NumberFormatException) { val errMsg = "Version components must consist of numbers." val exception = IllegalArgumentException(errMsg) exception.addSuppressed(e) throw exception } } if (suffix.isNullOrEmpty()) { messageCollector.report( CompilerMessageSeverity.WARNING, String.format(Locale.CHINA, "version = %d.%d.%d.%d", major, minor, patch, extra) ) } else { messageCollector.report( CompilerMessageSeverity.WARNING, String.format(Locale.CHINA, "version = %d.%d.%d.%d-%s", major, minor, patch, extra, suffix) ) } if (checkVersion(major) || checkVersion(minor) || checkVersion(patch) || checkVersion(extra)) { val msg = String.format( Locale.CHINA, "Version components are out of range: %d.%d.%d.%d.", major, minor, patch, extra ) throw IllegalArgumentException(msg) } return Multiple(major, minor, patch, extra, suffix) } private fun checkVersion(version: Int): Boolean { return version < MIN_COMPONENT_VALUE || version > MAX_COMPONENT_VALUE } }
Multiple
data class Multiple<out A, out B, out C, out D, out E>( val first: A, val second: B, val third: C, val fourth: D, val fifth: E? ) : Serializable { override fun toString(): String = "($first, $second, $third, $fourth, $fifth)" }
VersionMethodVisitor
class VersionMethodVisitor( api: Int, mv: MethodVisitor, private val major: Int, private val minor: Int, private val patch: Int, private val extra: Int, private val suffix: String? ) : MethodPatternAdapter(api, mv) { companion object { // state private const val SEEN_ICONST_0 = 1 private const val SEEN_ICONST_0_ICONST_0 = 2 private const val SEEN_ICONST_0_ICONST_0_ICONST_0 = 3 private const val SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 = 4 private const val SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL = 5 // Version fully qualified name private const val OWNER = "com/guodong/android/Version" private const val METHOD_NAME = "<init>" private const val METHOD_DESCRIPTOR = "(IIIILjava/lang/String;)V" } /** * val version = Version(0, 0, 0, 0, null) * ICONST_0 * ICONST_0 * ICONST_0 * ICONST_0 * ACONST_NULL */ override fun visitInsn(opcode: Int) { // State machine when (state) { SEEN_NOTHING -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0 return } } SEEN_ICONST_0 -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0_ICONST_0 return } } SEEN_ICONST_0_ICONST_0 -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0_ICONST_0_ICONST_0 return } } SEEN_ICONST_0_ICONST_0_ICONST_0 -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 return } } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 -> { if (opcode == Opcodes.ACONST_NULL) { state = SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL return } } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL -> { if (opcode == Opcodes.ACONST_NULL) { mv.visitInsn(opcode) return } } } super.visitInsn(opcode) } override fun visitMethodInsn( opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean ) { val flag = opcode == Opcodes.INVOKESPECIAL && OWNER == owner && METHOD_NAME == name && METHOD_DESCRIPTOR == descriptor when (state) { SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL -> { if (flag) { weaveCode(major) weaveCode(minor) weaveCode(patch) weaveCode(extra) weaveSuffix() state = SEEN_NOTHING } } } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) } // Reissue override fun visitInsn() { when (state) { SEEN_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ACONST_NULL) } } state = SEEN_NOTHING } // Woven version number private fun weaveCode(code: Int) { when { code <= 5 -> { val opcode = when (code) { 0 -> Opcodes.ICONST_0 1 -> Opcodes.ICONST_1 2 -> Opcodes.ICONST_2 3 -> Opcodes.ICONST_3 4 -> Opcodes.ICONST_4 5 -> Opcodes.ICONST_5 else -> Opcodes.ICONST_0 } mv.visitInsn(opcode) } code <= 127 -> { mv.visitIntInsn(Opcodes.BIPUSH, code) } else -> { mv.visitIntInsn(Opcodes.SIPUSH, code) } } } // Woven suffix private fun weaveSuffix() { if (suffix.isNullOrEmpty()) { mv.visitInsn(Opcodes.ACONST_NULL) } else { mv.visitLdcInsn(suffix) } } }
application
sample - build.gradle.kts
plugins { kotlin("jvm") id("com.guodong.android.version.kcp") } version { version = "1.0.0.1" }
Test
fun main() { println("version = ${Version.CURRENT}") } // output version = 1.0.0.1
happy~
reference resources
- Refer to kotlinversion kt ↩