Application of kotlin KCP - modify SDK version number

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

  1. Refer to kotlinversion kt

Tags: Android kotlin

Posted by henrygao on Mon, 23 May 2022 19:19:51 +0300