All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mcxiaoke.packer.ng.GradleTask.groovy Maven / Gradle / Ivy

package com.mcxiaoke.packer.ng

import com.android.build.gradle.api.BaseVariant
import com.mcxiaoke.packer.cli.Bridge
import groovy.io.FileType
import groovy.text.SimpleTemplateEngine
import groovy.text.Template
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

import java.text.SimpleDateFormat
import java.util.regex.Pattern

/**
 * User: mcxiaoke
 * Date: 15/11/23
 * Time: 14:40
 */
class GradleTask extends DefaultTask {

    @Input
    BaseVariant variant

    @Input
    GradleExtension extension

    GradleTask() {
        description = 'generate APK with channel info'
    }

    Template getNameTemplate() {
        String format
        String propValue = project.findProperty(Const.PROP_FORMAT)
        if (propValue != null) {
            format = propValue.toString()
        } else {
            format = extension.archiveNameFormat
        }
        if (format == null || format.isEmpty()) {
            format = Const.DEFAULT_FORMAT
        }
        def engine = new SimpleTemplateEngine()
        return engine.createTemplate(format)
    }

    File getOriginalApk() {
        variant.outputs.each { ot ->
            logger.info("Output APK: ${ot.name},${ot.outputFile}")
        }
        File file = variant.outputs[0].outputFile
        if (!Bridge.verifyApk(file)) {
            throw new PluginException("APK Signature Scheme v2 verify failed: '${file}'")
        }
        return file
    }

    File getOutputRoot() {
        File outputDir
        String propValue = project.findProperty(Const.PROP_OUTPUT)
        if (propValue != null) {
            String dirName = propValue.toString()
            outputDir = new File(project.rootDir, dirName)
        } else {
            outputDir = extension.archiveOutput
        }
        if (outputDir == null) {
            outputDir = new File(project.buildDir, Const.DEFAULT_OUTPUT)
        }
        if (!outputDir.exists()) {
            outputDir.mkdirs()
        }
        return outputDir
    }

    File getVariantOutput() {
        File outputDir = getOutputRoot()
        String flavorName = variant.flavorName
        if (flavorName.length() > 0) {
            outputDir = new File(outputDir, flavorName)
        }
        if (!outputDir.exists()) {
            outputDir.mkdirs()
        } else {
            logger.info(":${name} delete old APKs in ${outputDir.absolutePath}")
            // delete old APKs
            outputDir.eachFile(FileType.FILES) { file ->
                if (file.getName().endsWith(".apk")) {
                    file.delete()
                }
            }
        }
        return outputDir
    }

    Set getChannels() {
        // -P channels=ch1,ch2,ch3
        // -P [email protected]
        // channelList = [ch1,ch2,ch3]
        // channelFile = project.file("channels.txt")
        List channels = []
        // check command line property
        def propValue = project.findProperty(Const.PROP_CHANNELS)
        if (propValue != null) {
            String prop = propValue.toString()
            logger.info(":${project.name} channels property: '${prop}'")
            if (prop.startsWith("@")) {
                def fileName = prop.substring(1)
                if (fileName != null) {
                    File f = new File(project.rootDir, fileName)
                    if (!f.isFile() || !f.canRead()) {
                        throw new PluginException("channel file not exists: '${f.absolutePath}'")
                    }
                    channels = readChannels(f)
                } else {
                    throw new PluginException("invalid channels property: '${prop}'")
                }
            } else {
                channels = prop.split(",")
            }
            if (channels == null || channels.isEmpty()) {
                throw new PluginException("invalid channels property: '${prop}'")
            }
            return escape(channels)
        }
        if (extension.channelList != null) {
            channels = extension.channelList
            logger.info(":${project.name} ext.channelList: ${channels}")
        } else if (extension.channelMap != null) {
            String flavorName = variant.flavorName
            File f = extension.channelMap.get(flavorName)
            logger.info(":${project.name} extension.channelMap file: ${f}")
            if (f == null || !f.isFile()) {
                throw new PluginException("channel file not exists: '${f.absolutePath}'")
            }
            if (f != null && f.isFile()) {
                channels = readChannels(f)
            }
        } else if (extension.channelFile != null) {
            File f = extension.channelFile
            logger.info(":${project.name} extension.channelFile: ${f}")
            if (!f.isFile()) {
                throw new PluginException("channel file not exists: '${f.absolutePath}'")
            }
            channels = readChannels(f)
        }
        if (channels == null || channels.isEmpty()) {
            throw new PluginException("No channels found")
        }
        return escape(channels)
    }


    void showProperties() {
        logger.info("Extension: ${extension}")
        logger.info("Property: ${Const.PROP_CHANNELS} = " +
                "${project.findProperty(Const.PROP_CHANNELS)}")
        logger.info("Property: ${Const.PROP_OUTPUT} = " +
                "${project.findProperty(Const.PROP_OUTPUT)}")
        logger.info("Property: ${Const.PROP_FORMAT} = " +
                "${project.findProperty(Const.PROP_FORMAT)}")
    }

    @TaskAction
    void generate() {
        println("============================================================")
        println("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin")
        println("============================================================")
        showProperties()
        File apkFile = getOriginalApk()
        File rootDir = getOutputRoot()
        File outputDir = getVariantOutput()
        Collection channels = getChannels()
        Template template = getNameTemplate()
        println("Variant: ${variant.name}")
        println("Input: ${apkFile.path}")
        println("Output: ${outputDir.path}")
        println("Channels: [${channels.join('/')}]")
        for (String channel : channels) {
            File tempFile = new File(outputDir, "tmp-${channel}.apk")
            copyTo(apkFile, tempFile)
            try {
                Bridge.writeChannel(tempFile, channel)
                String apkName = buildApkName(channel, tempFile, template)
                File finalFile = new File(outputDir, apkName)
                if (Bridge.verifyChannel(tempFile, channel)) {
                    println("--> Generating: ${apkName}")
                    tempFile.renameTo(finalFile)
                    logger.info("Generated: ${finalFile}")
                } else {
                    throw new PluginException("${channel} APK verify failed")
                }
            } catch (IOException ex) {
                throw new PluginException("${channel} APK generate failed", ex)
            } finally {
                tempFile.delete()
            }
        }
        println("Outputs: ${rootDir.absolutePath}")
        println("============================================================")
    }

    String buildApkName(channel, file, template) {
        def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date())
        def fileSHA1 = HASH.sha1(file)
        def nameMap = [
                'appName'    : project.name,
                'projectName': project.rootProject.name,
                'fileSHA1'   : fileSHA1,
                'channel'    : channel,
                'flavor'     : variant.flavorName,
                'buildType'  : variant.buildType.name,
                'versionName': variant.versionName,
                'versionCode': variant.versionCode,
                'appPkg'     : variant.applicationId,
                'buildTime'  : buildTime
        ]
        logger.info("nameMap: ${nameMap}")
        return template.make(nameMap).toString() + '.apk'
    }

    static Set escape(Collection cs) {
        // filter invalid chars for filename
        Pattern pattern = ~/[\/:*?"'<>|]/
        return cs.collect { it.replaceAll(pattern, "_") }.toSet()
    }

    static List readChannels(File file) {
        List channels = []
        file.eachLine { line, number ->
            String[] parts = line.split('#')
            if (parts && parts[0]) {
                def c = parts[0].trim()
                if (c) {
                    channels.add(c)
                }
            }
        }
        return channels
    }

    static void copyTo(File src, File dest) {
        def input = src.newInputStream()
        def output = dest.newOutputStream()
        output << input
        input.close()
        output.close()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy