Skip to content

Instantly share code, notes, and snippets.

@tprochazka
Created November 15, 2018 19:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tprochazka/09bc1fdf6da0de7ad0830c911d10028d to your computer and use it in GitHub Desktop.
Save tprochazka/09bc1fdf6da0de7ad0830c911d10028d to your computer and use it in GitHub Desktop.
import com.android.annotations.NonNull
import com.android.build.gradle.api.ApkVariantOutput
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.internal.scope.TaskConfigAction
import com.avast.gradle.buildplugin.Config
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.security.SecureRandom
/**
* Upload APK to remote server via SCP a run remote script via SSH.
*
* @author Tomáš Procházka (prochazka)
*/
class RemoteSignApkTask extends DefaultTask {
private static final String SSH_CMD = "ssh"
private static final String SCP_CMD = "scp"
@InputFile
File inputFile
@OutputFile
File outputFile
ApplicationVariant applicationVariant
@TaskAction
void sign() {
def config = getConfig(project)
if (config == null) {
return
}
String sshCmd = config.sshCmd
String scpCmd = config.scpCmd
project.getLogger().lifecycle(" Remote signing... ")
if (inputFile == outputFile) {
// rename original input file if is it the same as output one
// it is needed if we want to keep same output name for artifact before and after sign
File unsignedFile = new File(inputFile.parent, inputFile.name.replace(".apk", ",unsigned.apk"))
inputFile.renameTo(unsignedFile)
inputFile = unsignedFile
}
// Generate a random string to create a unique apk names
SecureRandom random = new SecureRandom()
String uniqueBuildTag = new BigInteger(130, random).toString(32)
// Construct APK names
String srcApk = this.inputFile.getPath().replaceFirst("[A-Z]:", "")
String resultApk = this.outputFile.getPath().replaceFirst("[A-Z]:", "")
//(Note: replace is special hack for SCP on windows which doesn't allow c: in the path)
String dstApk = uniqueBuildTag + "_unsigned.apk"
String signedApk = uniqueBuildTag + "_signed.apk"
boolean signV2 = true // we are using just v2 already
// Construct command lists
String keyName = SignConfigurator.getRemoteSignKeyName(this.applicationVariant, project.android.getDefaultConfig())
String signingScript = "apksign.sh"
String[] uploadApkCmd = [scpCmd, srcApk, config.signHost + ":" + dstApk]
String[] signApkCmd = [sshCmd, config.signHost, "sudo $signingScript -k $keyName < " + dstApk + " > " + signedApk]
String[] downloadApkCmd = [scpCmd, config.signHost + ":" + signedApk, resultApk]
String[] cleanApkCmd = [sshCmd, config.signHost, "rm " + dstApk + "; rm " + signedApk]
this.callCommandLine(uploadApkCmd, "Uploading APK")
this.callCommandLine(signApkCmd, "Signing APK with '$keyName' certificate")
this.callCommandLine(downloadApkCmd, "Downloading APK")
this.callCommandLine(cleanApkCmd, "Cleanup")
}
static boolean checkConfig(Project project) {
Map config = getConfig(project)
if (config.signHost == null) {
println "REMOTE_SIGNING_USER_AND_HOST not configured. Skipping signing."
}
return config.signHost != null
}
private static Map getConfig(Project project) {
// from the legacy reason we will still use this env variables
String sshCmd = ConfigProvider.get(project, "AVAST_ANDROID_BUILD_SSH_CMD", SSH_CMD)
String scpCmd = ConfigProvider.get(project, "AVAST_ANDROID_BUILD_SCP_CMD", SCP_CMD)
String signHost = ConfigProvider.get(project, "REMOTE_SIGNING_USER_AND_HOST", Config.SIGNER_URL)
return ["sshCmd": sshCmd, "scpCmd": scpCmd, "signHost": signHost]
}
/**
* Call a command line
* @param line A list of strings forming a command line
* @param tag A string which will be displayed in a log
*/
private int callCommandLine(String[] line, String tag) {
logger.info("Signer CMD: " + line.join(" "))
def command = Runtime.getRuntime().exec(line.join(" "))
def result = command.waitFor()
if (result > 0) {
project.getLogger().error(command.errorStream.text.trim())
project.getLogger().lifecycle(" " + tag + " failed with " + command.exitValue())
} else {
project.getLogger().lifecycle(" " + tag + "... Success")
}
return command.exitValue()
}
/**
* Class responsible for sign task configuration
*/
static class ConfigAction implements TaskConfigAction<RemoteSignApkTask> {
private final ApkVariantOutput output
private final ApplicationVariant variant
@NonNull
@Override
String getName() {
return output.getTaskName("RemoteSignApkTask")
}
@NonNull
@Override
Class<RemoteSignApkTask> getType() {
return RemoteSignApkTask.class
}
ConfigAction(ApplicationVariant variant, ApkVariantOutput output) {
this.variant = variant
this.output = output
}
@Override
@TaskAction
void execute(@NonNull RemoteSignApkTask signTask) {
File assembleOutputFile = output.outputFile
signTask.outputFile = new File(assembleOutputFile.parent, assembleOutputFile.name.replaceFirst("[,-]unsigned", ""))
signTask.inputFile = signTask.outputFile
// hack to keep proper final name also when assemble task will be skipped
output.outputFileName = signTask.outputFile.getName()
signTask.applicationVariant = variant
// setup task dependencies
signTask.dependsOn output.packageApplication
output.assemble.dependsOn signTask
variant.outputsAreSigned = true
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment