├── src ├── test │ ├── resources │ │ ├── alt-zipalign │ │ │ └── zipalign │ │ ├── android │ │ │ └── build-tools │ │ │ │ └── 1.0 │ │ │ │ └── zipalign │ │ ├── win-android │ │ │ ├── build-tools │ │ │ │ └── 1.0 │ │ │ │ │ └── zipalign.exe │ │ │ └── tools │ │ │ │ └── android.bat │ │ ├── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ │ ├── SignApksBuilderTest.p12 │ │ ├── SignApksBuilderTest2.p12 │ │ ├── SignApksBuilderTestMulti.p12 │ │ ├── SignApksBuilderTest-exposed.p12 │ │ ├── SignApksBuilderTest-noKeyPass.p12 │ │ ├── workspace │ │ │ ├── SignApksBuilderTest.apk │ │ │ ├── SignApksBuilderTest-unsigned.apk │ │ │ ├── SignApksBuilderTest-chocolate_flavor.apk │ │ │ └── standard_gradle_proj │ │ │ │ └── app │ │ │ │ └── build │ │ │ │ └── outputs │ │ │ │ └── apk │ │ │ │ ├── app-debug-unsigned.apk │ │ │ │ └── app-release-unsigned.apk │ │ ├── org │ │ │ └── jenkinsci │ │ │ │ └── plugins │ │ │ │ └── androidsigning │ │ │ │ └── compatibility │ │ │ │ ├── SignApksBuilderCompatibility_2_0_8_Test.zip │ │ │ │ ├── SignApksBuilderCompatibility_2_1_0_Test.zip │ │ │ │ ├── config-2.1.0.xml │ │ │ │ ├── SignApksBuilderCompatibility_2_1_0_Test.LocalData │ │ │ │ └── jobs │ │ │ │ │ └── SignApksBuilderCompatibility_2_1_0_Test │ │ │ │ │ └── config.xml │ │ │ │ ├── SignApksBuilderCompatibility_2_0_8_Test.LocalData │ │ │ │ └── jobs │ │ │ │ │ └── SignApksBuilderCompatibility_2_0_8_Test │ │ │ │ │ └── config.xml │ │ │ │ └── config-2.0.8.xml │ │ ├── create_key_store.sh │ │ ├── SignApksBuilderTest2-public.pem │ │ ├── SignApksBuilderTest.pem │ │ ├── SignApksBuilderTest-key-exposed.pem │ │ ├── SignApksBuilderTest-key-exposed.pkcs8.pem │ │ ├── SignApksBuilderTest-key.pem │ │ ├── SignApksBuilderTest2-key.pem │ │ ├── SignApksBuilderTest-pair-exposed.pem │ │ ├── SignApksBuilderTest2-pair.pem │ │ └── SignApksBuilderTest-pair.pem │ ├── java │ │ └── org │ │ │ └── jenkinsci │ │ │ └── plugins │ │ │ └── androidsigning │ │ │ ├── AndroidSigningRules.java │ │ │ ├── BuildArtifact.java │ │ │ ├── TestSignedApkMapping.java │ │ │ ├── CopyFileCallable.java │ │ │ ├── CopyTestWorkspace.java │ │ │ ├── FakeZipalign.java │ │ │ ├── VerifyApkCallable.java │ │ │ ├── compatibility │ │ │ ├── SignApksBuilderCompatibility_2_1_0_Test.java │ │ │ └── SignApksBuilderCompatibility_2_0_8_Test.java │ │ │ ├── ApkArtifactIsSignedMatcher.java │ │ │ ├── TestKeyStore.java │ │ │ ├── ReadingKeyStoresTest.java │ │ │ ├── SignApksStepTest.java │ │ │ └── ZipalignToolTest.java │ └── groovy │ │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── androidsigning │ │ ├── UnsignedApkSiblingMappingTest.groovy │ │ ├── SignApksDslContextTest.groovy │ │ └── MultiEntryToSingleEntryBuilderMigrationTest.groovy └── main │ ├── resources │ ├── META-INF │ │ └── hudson.remoting.ClassFilter │ ├── index.jelly │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── androidsigning │ │ ├── SignApksStep │ │ └── config.jelly │ │ ├── SignApksBuilder │ │ ├── help-archiveUnsignedApks.html │ │ ├── help-skipZipalign.html │ │ ├── help-keyAlias.html │ │ ├── help-apksToSign.html │ │ ├── config.properties │ │ ├── help-androidHome.html │ │ ├── help-zipalignPath.html │ │ ├── help-archiveSignedApks.html │ │ ├── help-keyStoreId.html │ │ └── config.jelly │ │ ├── Messages.properties │ │ └── SignedApkMappingStrategy │ │ ├── UnsignedApkBuilderDirMapping │ │ └── help.jelly │ │ └── UnsignedApkSiblingMapping │ │ └── help.jelly │ └── java │ └── org │ └── jenkinsci │ └── plugins │ └── androidsigning │ ├── GetPathSeparator.java │ ├── MultiEntryToSingleEntryBuilderMigration.java │ ├── Apk.java │ ├── SignApksDslContext.java │ ├── SignedApkMappingStrategy.java │ ├── SigningComponents.java │ ├── SignApksStep.java │ ├── ZipalignTool.java │ └── SignApksBuilder.java ├── .gitignore ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .mvn ├── maven.config └── extensions.xml ├── android-signing.png ├── android-signing-advanced.png ├── Jenkinsfile ├── NOTICE ├── pom.xml ├── CHANGELOG.md └── README.md /src/test/resources/alt-zipalign/zipalign: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | work 5 | -------------------------------------------------------------------------------- /src/test/resources/android/build-tools/1.0/zipalign: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/win-android/build-tools/1.0/zipalign.exe: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/win-android/tools/android.bat: -------------------------------------------------------------------------------- 1 | echo "android tool" -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/android-signing-plugin-developers 2 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /android-signing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/android-signing.png -------------------------------------------------------------------------------- /android-signing-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/android-signing-advanced.png -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/SignApksBuilderTest.p12 -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest2.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/SignApksBuilderTest2.p12 -------------------------------------------------------------------------------- /src/main/resources/META-INF/hudson.remoting.ClassFilter: -------------------------------------------------------------------------------- 1 | org.jenkinsci.plugins.androidsigning.VerifyApkCallable 2 | java.security.cert.Certificate$CertificateRep 3 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTestMulti.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/SignApksBuilderTestMulti.p12 -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-exposed.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/SignApksBuilderTest-exposed.p12 -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-noKeyPass.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/SignApksBuilderTest-noKeyPass.p12 -------------------------------------------------------------------------------- /src/test/resources/workspace/SignApksBuilderTest.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/workspace/SignApksBuilderTest.apk -------------------------------------------------------------------------------- /src/test/resources/workspace/SignApksBuilderTest-unsigned.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/workspace/SignApksBuilderTest-unsigned.apk -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin(useArtifactCachingProxy: false, failFast: false, useContainerAgent: true, configurations: [ 2 | [platform: 'linux', jdk: 21], 3 | [platform: 'windows', jdk: 17], 4 | ]) 5 | -------------------------------------------------------------------------------- /src/test/resources/workspace/SignApksBuilderTest-chocolate_flavor.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/workspace/SignApksBuilderTest-chocolate_flavor.apk -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/AndroidSigningRules.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | /** 4 | * Created by stjohnr on 2/22/17. 5 | */ 6 | public class AndroidSigningRules { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | Sign Android APK bundles with a private key Jenkins manages and provides. 7 |
8 | -------------------------------------------------------------------------------- /src/test/resources/workspace/standard_gradle_proj/app/build/outputs/apk/app-debug-unsigned.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/workspace/standard_gradle_proj/app/build/outputs/apk/app-debug-unsigned.apk -------------------------------------------------------------------------------- /src/test/resources/workspace/standard_gradle_proj/app/build/outputs/apk/app-release-unsigned.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/workspace/standard_gradle_proj/app/build/outputs/apk/app-release-unsigned.apk -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_0_8_Test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_0_8_Test.zip -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_1_0_Test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/android-signing-plugin/master/src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_1_0_Test.zip -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-archiveUnsignedApks.html: -------------------------------------------------------------------------------- 1 |
2 | Check this option to add all input unsigned APKs to the build's archived artifacts. These will be the APK files selected with glob pattern(s) of the 3 | APKs to Sign setting. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-skipZipalign.html: -------------------------------------------------------------------------------- 1 |
2 | Skip the Zipalign step of signing the APK(s). 3 | This is primarily for the case of signing debug APKs, for which the zipalign command fails. 4 |
-------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates 2 | --- 3 | version: 2 4 | updates: 5 | - package-ecosystem: maven 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-keyAlias.html: -------------------------------------------------------------------------------- 1 |
2 | The entry name of the private key/certificate chain you want to use to sign your APK(s). This entry must exist in the key store credentials the 3 | Key Store ID references. If your key store contains only one key entry, which is the most common case, you can leave this field blank. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-apksToSign.html: -------------------------------------------------------------------------------- 1 |
2 | An Ant-style glob, or multiple comma-separated globs, selecting the APK files relative to the 3 | workspace. For example, myApp/build/outputs/apk/myApp-unsigned.apk or **/*-unsigned.apk or 4 | app1/**/*-unsigned.apk, app2/**/*-unsigned.apk. 5 |
-------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/BuildArtifact.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import hudson.model.FreeStyleBuild; 4 | import hudson.model.Run; 5 | 6 | 7 | class BuildArtifact { 8 | final FreeStyleBuild build; 9 | final Run.Artifact artifact; 10 | 11 | BuildArtifact(FreeStyleBuild build, Run.Artifact artifact) { 12 | this.build = build; 13 | this.artifact = artifact; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/config.properties: -------------------------------------------------------------------------------- 1 | field.androidHome=ANDROID_HOME Override 2 | field.zipalignPath=Zipalign Path 3 | field.entries=Signing Entries 4 | field.keyStoreId=Key Store 5 | field.keyAlias=Key Alias 6 | field.apksToSign=APKs to Sign 7 | field.signedApkMapping=Signed APK Destination 8 | field.skipZipalign=Skip Zipalign 9 | field.archiveSignedApks=Archive Signed APKs 10 | field.archiveUnsignedApks=Archive Unsigned APKs -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-androidHome.html: -------------------------------------------------------------------------------- 1 |
2 | Override the path of the Android SDK installation this build step should use to find 3 | the zipalign tool. You can also set the 4 | ANDROID_HOME environment variable in your Jenkins system or node configuration. E.g., /usr/local/android-sdk. 5 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/Messages.properties: -------------------------------------------------------------------------------- 1 | builderDisplayName=Sign Android APKs 2 | validation.noWorkspace=Unable to validate - this job does not yet have a workspace 3 | validation.noProject=Unable to validate - this step does not have a parent project 4 | validation.globSearchLimitReached=Unable to validate - the pattern searched too many files ({0,number,integer}) without a match 5 | signedApkMapping.builderDir.displayName=Output to separate directory 6 | signedApkMapping.unsignedSibling.displayName=Output to unsigned APK sibling -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-zipalignPath.html: -------------------------------------------------------------------------------- 1 |
2 | Override the full path of the Android zipalign executable this 3 | build step should use to align the target APKs. You can also set the 4 | ANDROID_ZIPALIGN environment variable in your Jenkins system or node configuration. E.g., /opt/android-tools/bin/zipalign 5 |
-------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/GetPathSeparator.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import hudson.remoting.VirtualChannel; 7 | import jenkins.MasterToSlaveFileCallable; 8 | 9 | public class GetPathSeparator extends MasterToSlaveFileCallable { 10 | private static final long serialVersionUID = 1; 11 | @Override 12 | public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { 13 | return File.pathSeparator; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-archiveSignedApks.html: -------------------------------------------------------------------------------- 1 |
2 | Check this option to add all signed APKs this build step generates 3 | to the build's archived artifacts. If you don't want this step to archive the signed APK artifacts, downstream build steps can access signed APKs in the 4 | workspace at paths like SignApksBuilder-out/myApp-unsigned.apk/myApp-signed.apk, where myApp-unsigned.apk is a directory named 5 | for the input unsigned APK. 6 |
-------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignedApkMappingStrategy/UnsignedApkBuilderDirMapping/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | Generate the signed APK in the job workspace under the directory SignApksBuilder-out/UNSIGNED_APK_NAME/. For example, 6 | WORKSPACE/build/outputs/apk/myApp-unsigned.apk -> WORKSPACE/SignApksBuilder-out/myApp-unsigned.apk/myApp-signed.apk. 7 |
8 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/help-keyStoreId.html: -------------------------------------------------------------------------------- 1 |
2 | The ID of a certificate credential. This build step expects the referenced 3 | certificate credential to be a password-protected PKCS12 file containing a key protected by the same password. See 4 | this plugin's script if 5 | you need help creating such a key store to add to Jenkins, or 6 | Google how to 7 | convert your Android release key to a PKCS12 file. 8 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignedApkMappingStrategy/UnsignedApkSiblingMapping/help.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Generate the signed APK in the same directory as the unsigned APK. For example, 5 | WORKSPACE/build/outputs/apk/myApp-unsigned.apk -> WORKSPACE/build/outputs/apk/myApp.apk. 6 | This matches the Android Gradle plugin's convention. 7 |

8 | If the unsigned APK does not end with -unsigned.apk, the signed APK name will end with -signed.apk. For example, 9 | WORKSPACE/build/outputs/apk/myApp-flavor.apk -> WORKSPACE/build/outputs/apk/myApp-flavor-signed.apk. 10 |

11 |
12 |
-------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/TestSignedApkMapping.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.jenkinsci.Symbol; 4 | import org.kohsuke.stapler.DataBoundConstructor; 5 | 6 | import edu.umd.cs.findbugs.annotations.NonNull; 7 | 8 | import hudson.Extension; 9 | import hudson.FilePath; 10 | import hudson.model.Descriptor; 11 | 12 | 13 | public class TestSignedApkMapping extends SignedApkMappingStrategy { 14 | 15 | @DataBoundConstructor 16 | public TestSignedApkMapping() { 17 | } 18 | 19 | @Override 20 | public FilePath destinationForUnsignedApk(FilePath unsignedApk, FilePath workspace) { 21 | return workspace.child(getClass().getSimpleName() + "-" + unsignedApk.getName()); 22 | } 23 | 24 | @Extension 25 | @Symbol("testSignedApkMapping") 26 | public static class DescriptorImpl extends Descriptor { 27 | @NonNull 28 | @Override 29 | public String getDisplayName() { 30 | return TestSignedApkMapping.class.getSimpleName(); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/CopyFileCallable.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.channels.FileChannel; 6 | import java.nio.file.StandardOpenOption; 7 | 8 | import hudson.remoting.VirtualChannel; 9 | import jenkins.MasterToSlaveFileCallable; 10 | 11 | 12 | class CopyFileCallable extends MasterToSlaveFileCallable { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | private final String destPath; 17 | 18 | CopyFileCallable(String destPath) { 19 | this.destPath = destPath; 20 | } 21 | 22 | @Override 23 | public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { 24 | long fileSize = f.length(); 25 | FileChannel inChannel = FileChannel.open(f.toPath(), StandardOpenOption.READ); 26 | FileChannel outChannel = FileChannel.open(new File(destPath).toPath(), 27 | StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.SYNC); 28 | inChannel.transferTo(0, fileSize, outChannel); 29 | outChannel.close(); 30 | inChannel.close(); 31 | System.out.printf("%s copied %s to %s", getClass().getSimpleName(), f.getAbsolutePath(), destPath); 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/resources/create_key_store.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | name=${1} 3 | [ -z ${name} ] && read -p "enter base file name for your key which will also be the entry name: " name 4 | echo "generating new self-signed certificate - openssl will ask you for a" 5 | read -p "private key encryption password (enter to continue)" 6 | echo "" 7 | openssl req -newkey rsa:2048 -keyout ${name}-key.pem -x509 -days 9999 -out ${name}-public.pem #-subj '/CN=SignApksBuilderTest/OU=android-signing/O=org.jenkins-ci.plugins/L=Jenkins/C=IO' 8 | echo "" 9 | echo "verifying generated certificate - openssl will ask you for the password" 10 | read -p "you just gave it to encrypt your new private key (enter to continue)" 11 | echo "" 12 | openssl x509 -text -noout -in ${name}-public.pem 13 | echo "" 14 | read -p "create p12 key store - you must provide a password to encrypt your key store (enter to continue)" 15 | echo "" 16 | cat ${name}-key.pem ${name}-public.pem > ${name}-pair.pem 17 | openssl pkcs12 -export -out ${name}.p12 -in ${name}-pair.pem -name ${name} 18 | echo "" 19 | echo "verify p12 keystore - you will need to provide the password you just used to" 20 | echo "encrypt your key store, as well as a one-time-use password that openssl uses" 21 | read -p "to encrypt the output of your private key to the console (enter to continue)" 22 | echo "" 23 | openssl pkcs12 -in ${name}.p12 -info 24 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/androidsigning/SignApksBuilder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/test/groovy/org/jenkinsci/plugins/androidsigning/UnsignedApkSiblingMappingTest.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning 2 | 3 | import hudson.FilePath 4 | import org.junit.Test 5 | 6 | import static org.hamcrest.CoreMatchers.equalTo 7 | import static org.junit.Assert.assertThat 8 | 9 | 10 | class UnsignedApkSiblingMappingTest { 11 | 12 | FilePath workspace = new FilePath(null, "/jenkins/jobs/UnsignedApkMappingTest/workspace") 13 | 14 | @Test 15 | void doesNotAddSignedSuffixWhenInputHasUnsignedSuffix() { 16 | SignedApkMappingStrategy.UnsignedApkSiblingMapping mapping = new SignedApkMappingStrategy.UnsignedApkSiblingMapping() 17 | FilePath inApk = workspace.child("app/build/outputs/app-unsigned.apk") 18 | FilePath outApk = mapping.destinationForUnsignedApk(inApk, workspace) 19 | 20 | assertThat(outApk, equalTo(workspace.child("app/build/outputs/app.apk"))) 21 | } 22 | 23 | @Test 24 | void addsSignedSuffixWhenInputApkDoesNotHaveUnsignedSuffix() { 25 | SignedApkMappingStrategy.UnsignedApkSiblingMapping mapping = new SignedApkMappingStrategy.UnsignedApkSiblingMapping() 26 | FilePath inApk = workspace.child("app/build/outputs/app-other.apk") 27 | FilePath outApk = mapping.destinationForUnsignedApk(inApk, workspace) 28 | 29 | assertThat(outApk, equalTo(workspace.child("app/build/outputs/app-other-signed.apk"))) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest2-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJzCCAw+gAwIBAgIJAO9DVq8SxH/aMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV 3 | BAYTAklPMRAwDgYDVQQIEwdKZW5raW5zMRAwDgYDVQQKEwdQbHVnaW5zMRgwFgYD 4 | VQQLEw9BbmRyb2lkIFNpZ25pbmcxHTAbBgNVBAMTFFNpZ25BcGtzQnVpbGRlclRl 5 | c3QyMB4XDTE3MDUyOTE0MjQ0NloXDTQ0MTAxMzE0MjQ0NlowajELMAkGA1UEBhMC 6 | SU8xEDAOBgNVBAgTB0plbmtpbnMxEDAOBgNVBAoTB1BsdWdpbnMxGDAWBgNVBAsT 7 | D0FuZHJvaWQgU2lnbmluZzEdMBsGA1UEAxMUU2lnbkFwa3NCdWlsZGVyVGVzdDIw 8 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3w0oFnoP3vk24zTLpeKf7 9 | S35iEUAmPeRl47z/xIFfoVUPVTv79/EwGroEHM3nGNwUjyqJRT+wdqugGIzFrGKP 10 | A13L7dJl4O2LQaqPlPkJoV9PTUrz0QltJrR6N4AwjIZ9hOfssK5JTPWRk3s3Q5fW 11 | zIYHXQwhZboP73lZ4I5lL6GaHpXullHq7G7ksGeEOYKoZQurKBUbxSd2WxKatHO2 12 | cNNg+4U9uC0iWGP5wXTtgxwiLE0yLmFR/R3h7nCqGtC4K+kJby+c9WXzkiUarr7G 13 | uD5N/ZAHsuhwJtST/w3A0R5/FCRxEGlmObAidxqyCngRnVDHAmcCnZBHuqB3P1bJ 14 | AgMBAAGjgc8wgcwwHQYDVR0OBBYEFADHFRIN+PQWzXFXKsM8idgQVla5MIGcBgNV 15 | HSMEgZQwgZGAFADHFRIN+PQWzXFXKsM8idgQVla5oW6kbDBqMQswCQYDVQQGEwJJ 16 | TzEQMA4GA1UECBMHSmVua2luczEQMA4GA1UEChMHUGx1Z2luczEYMBYGA1UECxMP 17 | QW5kcm9pZCBTaWduaW5nMR0wGwYDVQQDExRTaWduQXBrc0J1aWxkZXJUZXN0MoIJ 18 | AO9DVq8SxH/aMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAG2HqRQ1 19 | zvIW0liyyE7fYnIZDRnL9A3xLYXoy+kfkZngtLZt2vflmR0Zf9CSyEngCI1o8+bh 20 | HXQuQr5rxhQC6tCWt5LdQTtJRLKLSsi0YuvmUnou8O0IF7WfylZF2fRryAhxKWC/ 21 | MBIdP9MiZLGcxWdWAZDMG4r/ZCWOCfegY1Sy2xnTOfVrznI8HBdfblK/ePYHxccB 22 | Vb/EpJ79T6gySV6G9OxYz4l7DnmOucJ94DQi29vT5249srXO6sI9VYcTX5YVpSmW 23 | l0NNHcfXB8PszGJ+IR+LHhgS1FvIdv9H9bw2KELHlkhX82IT87wFG2xpqUkL77jb 24 | dacH2XrJeqvOxHY= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/config-2.1.0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | /usr/local/android-sdk-macosx/build-tools/23.0.3/zipalign 17 | android-team-1 18 | android-team-1 19 | SignApksBuilderTest-chocolate*.apk 20 | true 21 | true 22 | 23 | 24 | android-team-2 25 | android-team-2 26 | **/*-unsigned.apk 27 | true 28 | false 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEUTCCAzmgAwIBAgIJAKT/DjoEUfZEMA0GCSqGSIb3DQEBBQUAMHgxHDAaBgNV 3 | BAMTE1NpZ25BcGtzQnVpbGRlclRlc3QxGDAWBgNVBAsTD2FuZHJvaWQtc2lnbmlu 4 | ZzEfMB0GA1UEChMWb3JnLmplbmtpbnMtY2kucGx1Z2luczEQMA4GA1UEBxMHSmVu 5 | a2luczELMAkGA1UEBhMCSU8wHhcNMTcwMjAxMjMwMTAyWhcNNDQwNjE4MjMwMTAy 6 | WjB4MRwwGgYDVQQDExNTaWduQXBrc0J1aWxkZXJUZXN0MRgwFgYDVQQLEw9hbmRy 7 | b2lkLXNpZ25pbmcxHzAdBgNVBAoTFm9yZy5qZW5raW5zLWNpLnBsdWdpbnMxEDAO 8 | BgNVBAcTB0plbmtpbnMxCzAJBgNVBAYTAklPMIIBIjANBgkqhkiG9w0BAQEFAAOC 9 | AQ8AMIIBCgKCAQEAytDUAB0SN+S5hqFd+6wKNJqUq/R3ofbtzDUmdy7raigtfvqT 10 | 46iE+gRU6ztaHgO54jgfQ8ySyIh8Pk29PiO2fQjsOyvWRqCoKkbW6WfGxc/1mdTu 11 | Hj6ukevnK+HRAsDiF5IVHGO8sDPxbnoIhpE/uPW29n+vLyqS4iAOKpqbOtiGyAMz 12 | Jn/Cn5NddmlM2xhNNkhiNEBwY6zlI4sltw2jYYUXuEu6IU8S4dAlobHLxDbj4ZG4 13 | O0XmPjSomIl3n6jF3HhdA6uiU2AJd4z6Vq63uUB21/wet7zx1rGKht5yNFduDdok 14 | JTgGrmYc1GKD0l0XP2X7Zyeyl3jZpSl61mSNUwIDAQABo4HdMIHaMB0GA1UdDgQW 15 | BBS25sctiYe1nG32+NCPX7wNou6XOzCBqgYDVR0jBIGiMIGfgBS25sctiYe1nG32 16 | +NCPX7wNou6XO6F8pHoweDEcMBoGA1UEAxMTU2lnbkFwa3NCdWlsZGVyVGVzdDEY 17 | MBYGA1UECxMPYW5kcm9pZC1zaWduaW5nMR8wHQYDVQQKExZvcmcuamVua2lucy1j 18 | aS5wbHVnaW5zMRAwDgYDVQQHEwdKZW5raW5zMQswCQYDVQQGEwJJT4IJAKT/DjoE 19 | UfZEMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAF99HJstuOqIi3KN 20 | vvQEnJ00Z0iyglIZQhw43uKVkkUyKZhiQFgwIDvQEMJEVBHa9gLg0QwistBZOv9h 21 | Atqyt/vlAY5Y6iXtqLY9sVT0bHfGnF4FRoi9NVxpJp+EqZeC6WdKesnU6uU2fXdS 22 | LtczC37GsvUlLD3rhj5BbvDTW1CzkSezOjDJJkwjqm4eh9v95lf0XlGj+9A1OWEw 23 | /SfkpDw3F87xR5djY05WITS3zSDdXLPAZ37KNOnra/avu9kKQrakHDD1bVekxm4n 24 | ML/VWDol5jqYCjn23kNUyxTU/y42dCrCVUkYUM9GliAW00vUxWIFpMrSbAHd+/vv 25 | cDDXd9k= 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_1_0_Test.LocalData/jobs/SignApksBuilderCompatibility_2_1_0_Test/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | /usr/local/android-sdk-macosx/build-tools/23.0.3/zipalign 17 | android-team-1 18 | android-team-1 19 | build/outputs/apk/*-unsigned.apk 20 | true 21 | true 22 | 23 | 24 | android-team-2 25 | android-team-2 26 | **/*-unsigned.apk 27 | true 28 | false 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | This is a Derivative Work based on a Work originally created by 3 | Big Nerd Ranch. The original Work Source form was obtained from 4 | https://github.com/bignerdranch/jenkins-android-signing. The 5 | following original copyright notice by Big Nerd Ranch applies to 6 | all unmodified Source in this derivative work. 7 | 8 | Copyright 2015 Big Nerd Ranch 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | All Derivative Work modifications of the original Source are subject 23 | to the following copyright and license. 24 | 25 | Copyright (c) 2016, BIT Systems 26 | 27 | Licensed under the Apache License, Version 2.0 (the "License"); 28 | you may not use this file except in compliance with the License. 29 | You may obtain a copy of the License at 30 | 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | 33 | Unless required by applicable law or agreed to in writing, software 34 | distributed under the License is distributed on an "AS IS" BASIS, 35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 36 | See the License for the specific language governing permissions and 37 | limitations under the License. 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-key-exposed.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAytDUAB0SN+S5hqFd+6wKNJqUq/R3ofbtzDUmdy7raigtfvqT 3 | 46iE+gRU6ztaHgO54jgfQ8ySyIh8Pk29PiO2fQjsOyvWRqCoKkbW6WfGxc/1mdTu 4 | Hj6ukevnK+HRAsDiF5IVHGO8sDPxbnoIhpE/uPW29n+vLyqS4iAOKpqbOtiGyAMz 5 | Jn/Cn5NddmlM2xhNNkhiNEBwY6zlI4sltw2jYYUXuEu6IU8S4dAlobHLxDbj4ZG4 6 | O0XmPjSomIl3n6jF3HhdA6uiU2AJd4z6Vq63uUB21/wet7zx1rGKht5yNFduDdok 7 | JTgGrmYc1GKD0l0XP2X7Zyeyl3jZpSl61mSNUwIDAQABAoIBAQC5knv4IqFxzPFI 8 | U0wIJDEuUqZn9Aamhqasq5EniiHS/zIptiMtMhuCHAaLOcJKJkSlzY4l3gAMRn3Q 9 | EBdwzQKDw29K8OBdvVBBZpHr/I1un8psV90MhXP7hmr9xuAUceItiPeSA1w5qT1m 10 | RXiZkDtLBGDFiK0FLiI5jvUHXHFeU+7Ty7NFkt1sE7U+0gLKn5e8ClVzqSE+r3DX 11 | aSjdWsjH6PXCjWRTGGsr63CWFVc75mJaL3GZsI55XQQyUgNdlzv3DRfMb6NQjNJy 12 | UEjEgwtl79mW1H9qmQmDL9LnOMesP+Ka7CQhcMLHVECIJKc52DGcudkmdQxUOKZR 13 | DaIYL70RAoGBAP8P6k1mIABvUoU0GKYXNkRR+YLuFOgG/aPxbE5o8jtMXDZV2Vf0 14 | uSwYbSSsqBHyAzVWoF7Z5DgHWbXhS9xk7xmM+Kro+F2iA9sVMlHFeW1s9IXmfy+6 15 | oxmK3WuuP3PB/0y/Mdkyu8U57Y6Heb2VEEEwhors6TrkdYiXEt7uCp9JAoGBAMuP 16 | vAEs5CQPJTsAnJNlxrpJEG3rYkoN8yqKPXmvP7t00d4sBd/2eZ8Y+EFtgWBM1K3e 17 | vACvDuaffGalx2Yl/MgoM3L/57l6pEdamj0ZwPF6cfbdsiJ9yvI2mu0cPMG+TqhP 18 | xukytx4ttIrZaSRfSyxrUcbF4CQ2+hn62yFnqZu7AoGBANW8+YRIq1KR9x3mrS0p 19 | 0HDqHOPqLRzPFue2XSNL8IlekPt7b3m0eyQHiBaulN0M6EFfSV6SyxtklXnDxXV4 20 | I6FOr+dQ+ShFp2OE3LkHeZ0IK9S1dimCBkFWS/x7dXLEw/MFWXmAeTdqNrc0sgD7 21 | lDZ8upJau4t9fTysFMU5xy75AoGBAMZ/o+I1h0bOagyuHQDy1yXih53YUaFLFxsd 22 | cLMPPIOsd9ZBcX0i2RhWfgc3JFjmsuHVd9jm3A3x6ZojAF1Qn74CzaDPgIRy3m0i 23 | IZOBYI9ZSnZjWwidR+CHdO3QgkKfNA6WtK3EIaLRCOP4+7lXH3PyNu0xGc/WuG3L 24 | HBHoBxFvAoGABjwtoo/z2vVX+jc9scnsJdCz71+m929jYcW4sk1O0IoNSRb63moz 25 | Yj5YovLYgPvjmB3SI3F0fx6i9TR4hqmFbc9aLr0aL8Eby4kOjwpG3XxNpXQgSAAi 26 | h9+0NgaQCpPZjMstzJu513+rYoyd8RUQdaTodNqTgQxgTIZG4hHvpVM= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-key-exposed.pkcs8.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDK0NQAHRI35LmG 3 | oV37rAo0mpSr9Heh9u3MNSZ3LutqKC1++pPjqIT6BFTrO1oeA7niOB9DzJLIiHw+ 4 | Tb0+I7Z9COw7K9ZGoKgqRtbpZ8bFz/WZ1O4ePq6R6+cr4dECwOIXkhUcY7ywM/Fu 5 | egiGkT+49bb2f68vKpLiIA4qmps62IbIAzMmf8Kfk112aUzbGE02SGI0QHBjrOUj 6 | iyW3DaNhhRe4S7ohTxLh0CWhscvENuPhkbg7ReY+NKiYiXefqMXceF0Dq6JTYAl3 7 | jPpWrre5QHbX/B63vPHWsYqG3nI0V24N2iQlOAauZhzUYoPSXRc/ZftnJ7KXeNml 8 | KXrWZI1TAgMBAAECggEBALmSe/gioXHM8UhTTAgkMS5Spmf0BqaGpqyrkSeKIdL/ 9 | Mim2Iy0yG4IcBos5wkomRKXNjiXeAAxGfdAQF3DNAoPDb0rw4F29UEFmkev8jW6f 10 | ymxX3QyFc/uGav3G4BRx4i2I95IDXDmpPWZFeJmQO0sEYMWIrQUuIjmO9QdccV5T 11 | 7tPLs0WS3WwTtT7SAsqfl7wKVXOpIT6vcNdpKN1ayMfo9cKNZFMYayvrcJYVVzvm 12 | YlovcZmwjnldBDJSA12XO/cNF8xvo1CM0nJQSMSDC2Xv2ZbUf2qZCYMv0uc4x6w/ 13 | 4prsJCFwwsdUQIgkpznYMZy52SZ1DFQ4plENohgvvRECgYEA/w/qTWYgAG9ShTQY 14 | phc2RFH5gu4U6Ab9o/FsTmjyO0xcNlXZV/S5LBhtJKyoEfIDNVagXtnkOAdZteFL 15 | 3GTvGYz4quj4XaID2xUyUcV5bWz0heZ/L7qjGYrda64/c8H/TL8x2TK7xTntjod5 16 | vZUQQTCGiuzpOuR1iJcS3u4Kn0kCgYEAy4+8ASzkJA8lOwCck2XGukkQbetiSg3z 17 | Koo9ea8/u3TR3iwF3/Z5nxj4QW2BYEzUrd68AK8O5p98ZqXHZiX8yCgzcv/nuXqk 18 | R1qaPRnA8Xpx9t2yIn3K8jaa7Rw8wb5OqE/G6TK3Hi20itlpJF9LLGtRxsXgJDb6 19 | GfrbIWepm7sCgYEA1bz5hEirUpH3HeatLSnQcOoc4+otHM8W57ZdI0vwiV6Q+3tv 20 | ebR7JAeIFq6U3QzoQV9JXpLLG2SVecPFdXgjoU6v51D5KEWnY4TcuQd5nQgr1LV2 21 | KYIGQVZL/Ht1csTD8wVZeYB5N2o2tzSyAPuUNny6klq7i319PKwUxTnHLvkCgYEA 22 | xn+j4jWHRs5qDK4dAPLXJeKHndhRoUsXGx1wsw88g6x31kFxfSLZGFZ+BzckWOay 23 | 4dV32ObcDfHpmiMAXVCfvgLNoM+AhHLebSIhk4Fgj1lKdmNbCJ1H4Id07dCCQp80 24 | Dpa0rcQhotEI4/j7uVcfc/I27TEZz9a4bcscEegHEW8CgYAGPC2ij/Pa9Vf6Nz2x 25 | yewl0LPvX6b3b2NhxbiyTU7Qig1JFvreajNiPlii8tiA++OYHdIjcXR/HqL1NHiG 26 | qYVtz1ouvRovwRvLiQ6PCkbdfE2ldCBIACKH37Q2BpAKk9mMyy3Mm7nXf6tijJ3x 27 | FRB1pOh02pOBDGBMhkbiEe+lUw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/CopyTestWorkspace.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.kohsuke.stapler.DataBoundConstructor; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.net.URISyntaxException; 8 | import java.net.URL; 9 | 10 | import hudson.AbortException; 11 | import hudson.EnvVars; 12 | import hudson.Extension; 13 | import hudson.FilePath; 14 | import hudson.Launcher; 15 | import hudson.model.AbstractProject; 16 | import hudson.model.Run; 17 | import hudson.model.TaskListener; 18 | import hudson.tasks.BuildWrapperDescriptor; 19 | import jenkins.tasks.SimpleBuildWrapper; 20 | 21 | 22 | public class CopyTestWorkspace extends SimpleBuildWrapper { 23 | 24 | @DataBoundConstructor 25 | public CopyTestWorkspace() { 26 | } 27 | 28 | @Override 29 | public void setUp(Context context, Run build, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException { 30 | URL workspaceUrl = getClass().getResource("/workspace"); 31 | FilePath sourceWorkspace; 32 | try { 33 | sourceWorkspace = new FilePath(new File(workspaceUrl.toURI())); 34 | } 35 | catch (URISyntaxException e) { 36 | e.printStackTrace(listener.getLogger()); 37 | throw new AbortException(e.getMessage()); 38 | } 39 | sourceWorkspace.copyRecursiveTo("*/**", workspace); 40 | } 41 | 42 | @Extension 43 | public static class DescriptorImpl extends BuildWrapperDescriptor { 44 | @Override 45 | public boolean isApplicable(AbstractProject item) { 46 | return true; 47 | } 48 | 49 | @Override 50 | public String getDisplayName() { 51 | return getClass().getSimpleName(); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,330C635939CB5235 4 | 5 | DKCPOdNDM2F0riXPs2o0aPeggN44vYEWUwlSjVzN8jP1NyRPCxbHkWX3m/+F7LvW 6 | abNOXSl7Bv0DCWikmyJupWNQhskQcuB2W4jobvvSojLwOJbBy5rYN5vHUTOc1JlX 7 | wyeWMUYDv/2/otxfMSoNMIX+uVP2WuIxpT/JBKt4KLiPAq0IJ8kk3nFcBOMScOv/ 8 | o80fS7RuWvgqsw3Gap829aTozLMeoHX6cIN+yEuDEgvSDSy5nbKRnqtlvBXLmYGe 9 | ZH7MPVLIqKxjub/ccAAhy7WIav91yjboSqZc5l7qGN/MTg8oxhuzw5Uq1bEYY70E 10 | ZlANzmn8bcxz0hUKCS4lauOAxoNScTRfD2Eq5qt/ltzcR0FSt4WCrRK8gA8WwLXg 11 | 8X2oyJYXQRciAev3I9fwDGIdGI45SNa0vZu+qP7Qrq7GRtnl5Lbsd+KCKhPZWqV5 12 | 00nKN0Qtpd7bpOsq73xCNV+qjhp8PfabnlozSEx9NxA+arW/1L71MIJmcDsiqku8 13 | F+Nc9L3QKXFFjMYswAoqfYo5yHtdchkS22iKnjLeJa3yYm8doFbIniQz8VXr2aUg 14 | Lguf3rNB9jKWpG0263E9juWeXuTWbcuMDIwZ4eCemUF/ZixQWt3QrHIBAVGPWu2j 15 | A1FgGMl0Jpitd4h5OMKA7ok/m9ycVOY1137qiebmngH5xD+QnW9Fe8/IsC1S4CEr 16 | QYOulkzrh7jzEZjnSQr35ftY7BhN0CA1U2KzoSV8zUMkENPiIlu8TvLXZ6/eb8lp 17 | Ds4FJLTvXkIZHjcDHLNVnolDH/Y5ZfeAl0pGPtv40dUkX2DHKpkobD2pW15NiR6K 18 | qr4iR8HJltYf+TDk+7acro8YMvExtgZsHm4j6dXnE7sF6E7xKdFOD+37HDOYL+r3 19 | xF7+xNwEm7KC4LxhqfncceYOt1n6i5mXSzVXHCRA+zOnDWJPzMNJ7ntEEG8iIr9s 20 | c9q5Zk623glC9qeMsk74XmkYvg8oBuPq009o8TBz1IDZ+hpLGp+GdWTy87Z9IXiq 21 | 9P5RFcmCiFP1eBKGUnv/OiHf1oulIwH7DmgeVbg83+u2PM4FxvnrilSLTk8Faxz+ 22 | ih62d1P7L4IbOnflM3Mu4bOwLUb+zGWOBQ2sd2ftD90icNVwKL2n11GKQ8Fonzkz 23 | 8QbNaZFWmcOG7JxkfyMMarK7go3qqipji/2yvATsDkDaU7EeLoFDSs/vuQ5DEZWO 24 | 9I22Myvq+gC9CwbajASYSLObOyOq1DklmWYpkU8iBEoN8tN1bjDndKiMC6zSCq/d 25 | boSRNsgBGu5lpR7DeVzANj4PCiDqmJp73qWhhFkz8yOgc/J6pdocFwfWPldw73Mh 26 | MNH4wIXu0nBpAqmZ7C0ye5FbykBuALOA0IPkO7ocykbNw+dBTvdHTx/lMYEX3gRc 27 | cTNYOCEZ7WBQHAkF7L3BtlpW23n46Jq9bjC/LxG12tQe3fXoTkXBgVcLjUeaqjQN 28 | TNu3tWHSFtou2z1xweatNi2E2mJx7gnFiJIYLJjfzdS3+FEPtqhpPC7/NPKCpU5/ 29 | I5wMOoIWjIPkxBM5Tp026wwdKZ0CudtWUZ0JDKAFQGXb1PPcv+kaToUAeW98/pTZ 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest2-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,A8A85BE30D2A0C6C 4 | 5 | m2QjVKbNDKl1JKYhXn1PXMTKvxOPXlN1cG7U3hdaeG99eGxX20FaMZ0TZUsCoPmh 6 | 1RSsHBuRrRxjZsV3V+QJMWCRibZX9j/r9sYEXkfUblwF8+Ye9ZkZJqLWbLozDaCe 7 | 4hErZhmNflHMekrNmk0Z8qJvW3v5wC8oontknM6WWNqvWiQia4D5l5TqHFFzjzej 8 | Bg2OLGlNEhPG0oAlNzRHxXiNO8f3VExVidNEOOKsE93HseGfnBBDDVvucMoHYq8O 9 | wGUv3mbYU9K3ZQlzVGf/lYMXOws1pkmttyyffbF/N3ZEI/p9Em75szjXems02yX3 10 | ley1k9qM1ijsA9aa7kW+YXTWnrsM0ixvLjKN5BEVz6yiWtINpah++ABUZh7wi82S 11 | Aqt6DIM4R+PQRdDSue0HDDJRg5wHKoqDfIOrsRgC2AsE2VMh3nR+Rp/Gxd0qOrjy 12 | pypiTJY1jjWlolF4nI05ut+MnVtGWnqewNjRtkl8gm1efgnBu3pnBZIo5d8gB/2P 13 | VhzjUV/zRoA6v9xD1UXzJ8C43SaTLOBz85uXeOGyny7paxIlFHupby2VAnAks4Oh 14 | AGDIGeG2KGLTPMWp9H6Nmjt+um+gGjXN8ilbM7v3oaTbOcSKZXc9Jq1iVWPH5Y2b 15 | 5CXAFY0TYYYEUxGZGIFDN2sqMqnvv1xkMIO41RmQs5QYgGkmEvPPfxt31EevNj8e 16 | r47bnVB80AEA+1/Wye+JjhpfsV7J17h2znZtOewaXZPgGEEyWhGZY91d/OBJMB+l 17 | bdlcYTnsQdUYf2FRRPRN4lKoERP2Z/faE3/1ZxbW3tu46f8lAFL6PXzb2DwrRSCt 18 | oLbmy6kHeMeNjx9QGeNHVlY7YtaBEEeKZMI5E64PlBtjvD1hQlPr+8lxp+//0h2+ 19 | SGIjcqFMC4DZpSzI69TecMS0tADGlK3OUWnz6cZwBvorvNDLXKeUzsol9Js/YRn4 20 | unx9cQUfa/38iHc++oleHzxu5UT8+6GmpJFV/2KmgR21k5BLQityAC7g+WD/QeUn 21 | oWxZgrvnL1yUw7dwMHH76hVGEsAzlpSkA/fgaFWJVltEGRMTSZdY27RrkfeF3azU 22 | AsrsQHLfB5lq1N9b3nkaVM2ODegBpkcSMg9J2Q1OZ6Piq3vlYWQ+qWZ43g7J6u91 23 | UEe7J+YuQvqW6bkRymcL4NK0Bd9kUu8IOPEcgXfyAGu67OLka2w0RHHNDeIq8r5G 24 | WHOezFe+koaAX2FY+39O4FeoARBuMYa0Xrgc6XCarTts94mHacKgnJG7T5MWVPqA 25 | 5TSuOc0SoS3MfOSaJAXzXRB5hqpjy5lokZjVfNgKKcrso26JGez4kUmK+3DI7PH7 26 | d+bqLhnUBUr55+WKrt3Jn4zXlmnvnCGE0vOeQor+w6CNwSuSrE+Ajcmqq4KmmIxd 27 | NiFGY8wF5e9Xk5cTNtB5vR1wliqKu3Vb8ldVV0hMtEJ0xiTtw26p4uM/1jNMTEnk 28 | iVf2r5ooJ1Gy4W9zgM1ohIdLODojo4PaYzOKspkntwnfKn8QkDFUsFoqYN56x9zJ 29 | ZHnxZCD0d6Ugp9bk4l4wt/kPcg6jjLHxbVpr0PDjZojvyWDOxQaAKlExJLU+eX4C 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_0_8_Test.LocalData/jobs/SignApksBuilderCompatibility_2_0_8_Test/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | 17 | 18 | android-signing-1 19 | key1 20 | build/outputs/apk/*-unsigned.apk 21 | true 22 | true 23 | 24 | 25 | android-signing-1 26 | key2 27 | SignApksBuilderTest.apk, SignApksBuilderTest-choc*.apk 28 | false 29 | true 30 | 31 | 32 | android-signing-2 33 | key1 34 | **/*.apk 35 | false 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/MultiEntryToSingleEntryBuilderMigration.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | import hudson.Extension; 11 | import hudson.model.Descriptor; 12 | import hudson.model.Project; 13 | import hudson.model.listeners.ItemListener; 14 | import hudson.tasks.Builder; 15 | import hudson.util.DescribableList; 16 | import jenkins.model.Jenkins; 17 | 18 | 19 | @Extension 20 | public class MultiEntryToSingleEntryBuilderMigration extends ItemListener { 21 | 22 | private static final Logger log = Logger.getLogger(MultiEntryToSingleEntryBuilderMigration.class.getName()); 23 | 24 | @Override 25 | public void onLoaded() { 26 | Jenkins jenkins = Jenkins.get(); 27 | List jobs = jenkins.getAllItems(Project.class); 28 | for (Project job : jobs) { 29 | migrateBuildersOfJob(job); 30 | } 31 | } 32 | 33 | private void migrateBuildersOfJob(Project job) { 34 | DescribableList> old = job.getBuildersList(); 35 | boolean isMigrated = old.stream().allMatch(builder -> { 36 | if (builder instanceof SignApksBuilder) { 37 | return ((SignApksBuilder) builder).isMigrated(); 38 | } 39 | return true; 40 | }); 41 | if (isMigrated) { 42 | return; 43 | } 44 | final List migrated = new ArrayList<>(); 45 | for (Builder builder : old) { 46 | if (builder instanceof SignApksBuilder) { 47 | migrated.addAll(SignApksBuilder.singleEntryBuildersFromEntriesOfBuilder((SignApksBuilder) builder)); 48 | } 49 | else { 50 | migrated.add(builder); 51 | } 52 | } 53 | try { 54 | job.getBuildersList().replaceBy(migrated); 55 | } 56 | catch (IOException e) { 57 | log.log(Level.WARNING, "error migrating " + SignApksBuilder.class.getSimpleName() + " steps of job " + job, e); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/FakeZipalign.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.jvnet.hudson.test.FakeLauncher; 4 | 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | import java.util.List; 8 | 9 | import hudson.FilePath; 10 | import hudson.Launcher; 11 | import hudson.Proc; 12 | 13 | 14 | class FakeZipalign implements FakeLauncher { 15 | 16 | Launcher.ProcStarter lastProc; 17 | 18 | @Override 19 | public Proc onLaunch(Launcher.ProcStarter p) throws IOException { 20 | if (!p.cmds().get(0).contains("zipalign")) { 21 | return new FinishedProc(0); 22 | } 23 | lastProc = p; 24 | PrintStream logger = new PrintStream(p.stdout()); 25 | List cmd = p.cmds(); 26 | String inPath = cmd.get(cmd.size() - 2); 27 | String outPath = cmd.get(cmd.size() - 1); 28 | FilePath workspace = p.pwd(); 29 | FilePath in = workspace.child(inPath); 30 | FilePath out = workspace.child(outPath); 31 | try { 32 | out.getParent().mkdirs(); 33 | if (!out.getParent().isDirectory()) { 34 | throw new IOException("destination directory does not exist: " + out.getParent()); 35 | } 36 | logger.printf("FakeZipalign copy %s to %s in pwd %s%n", in.getRemote(), out.getRemote(), workspace); 37 | System.setProperty("sun.io.serialization.extendedDebugInfo", "true"); 38 | in.act(new CopyFileCallable(out.getRemote())); 39 | // TODO: this was resulting in incomplete copies and failing tests, for some reason 40 | // sometimes the output file would not have been completely written and reading the 41 | // aligned apk was failing 42 | // in.copyTo(out); 43 | logger.printf("FakeZipalign copy complete%n"); 44 | if (!out.exists()) { 45 | throw new IOException("FakeZipalign copy output does not exist: " + out.getRemote()); 46 | } 47 | long outSize = out.length(), inSize = in.length(); 48 | if (outSize != inSize) { 49 | throw new IOException("FakeZipalign copy output size " + outSize + " is different from input size " + inSize); 50 | } 51 | return new FinishedProc(0); 52 | } 53 | catch (InterruptedException e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/Apk.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.kohsuke.stapler.DataBoundConstructor; 4 | import org.kohsuke.stapler.DataBoundSetter; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import edu.umd.cs.findbugs.annotations.NonNull; 10 | 11 | import hudson.Extension; 12 | import hudson.model.AbstractDescribableImpl; 13 | import hudson.model.Descriptor; 14 | 15 | 16 | @Deprecated 17 | public final class Apk extends AbstractDescribableImpl { 18 | private String keyStore; 19 | private String alias; 20 | private String apksToSign; 21 | private boolean archiveSignedApks = true; 22 | private boolean archiveUnsignedApks = false; 23 | 24 | // renamed fields 25 | @SuppressWarnings("unused") 26 | transient private String selection; 27 | 28 | /** 29 | * 30 | * @param keyStore an ID of a {@link com.cloudbees.plugins.credentials.common.StandardCertificateCredentials} 31 | * @param alias the alias of the signing key in the key store 32 | * @param apksToSign an Ant-style glob pattern; multiple globs separated by commas are allowed 33 | */ 34 | @DataBoundConstructor 35 | public Apk(String keyStore, String alias, String apksToSign) { 36 | this.keyStore = keyStore; 37 | this.alias = alias; 38 | this.apksToSign = apksToSign; 39 | } 40 | 41 | @DataBoundSetter 42 | public void setArchiveSignedApks(boolean x) { 43 | archiveSignedApks = x; 44 | } 45 | 46 | @DataBoundSetter 47 | public void setArchiveUnsignedApks(boolean x) { 48 | archiveUnsignedApks = x; 49 | } 50 | 51 | public Apk archiveSignedApks(boolean x) { 52 | setArchiveSignedApks(x); 53 | return this; 54 | } 55 | 56 | public Apk archiveUnsignedApk(boolean x) { 57 | setArchiveUnsignedApks(x); 58 | return this; 59 | } 60 | 61 | @SuppressWarnings("unused") 62 | protected Object readResolve() { 63 | if (apksToSign == null) { 64 | apksToSign = selection; 65 | } 66 | return this; 67 | } 68 | 69 | @Extension 70 | public static class DescriptorImpl extends Descriptor { 71 | @Override @NonNull 72 | public String getDisplayName() { 73 | return "APK Signing Entry"; 74 | } 75 | } 76 | 77 | public String getApksToSign() { 78 | return apksToSign; 79 | } 80 | 81 | public String getKeyStore() { 82 | return keyStore; 83 | } 84 | 85 | public String getAlias() { 86 | return alias; 87 | } 88 | 89 | public boolean getArchiveUnsignedApks() { 90 | return archiveUnsignedApks; 91 | } 92 | 93 | public boolean getArchiveSignedApks() { 94 | return archiveSignedApks; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/VerifyApkCallable.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import com.android.apksig.ApkSigner; 4 | import com.android.apksig.ApkVerifier; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.Serializable; 9 | import java.security.PrivateKey; 10 | import java.security.cert.Certificate; 11 | import java.security.cert.X509Certificate; 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | import hudson.AbortException; 17 | import hudson.model.TaskListener; 18 | import hudson.remoting.VirtualChannel; 19 | import jenkins.MasterToSlaveFileCallable; 20 | 21 | 22 | class VerifyApkCallable extends MasterToSlaveFileCallable { 23 | 24 | private static final long serialVersionUID = 1; 25 | 26 | public static class VerifyResult implements Serializable { 27 | 28 | boolean isVerified; 29 | boolean isVerifiedV1Scheme; 30 | boolean isVerifiedV2Scheme; 31 | boolean isVerifiedV3Scheme; 32 | boolean containsErrors; 33 | X509Certificate[] certs; 34 | String[] warnings = new String[0]; 35 | String[] errors = new String[0]; 36 | 37 | public VerifyResult(ApkVerifier.Result result) { 38 | this.isVerified = result.isVerified(); 39 | this.isVerifiedV1Scheme = result.isVerifiedUsingV1Scheme(); 40 | this.isVerifiedV2Scheme = result.isVerifiedUsingV2Scheme(); 41 | this.isVerifiedV3Scheme = result.isVerifiedUsingV3Scheme(); 42 | this.certs = result.getSignerCertificates().toArray(new X509Certificate[0]); 43 | this.containsErrors = result.containsErrors(); 44 | List messages = new ArrayList<>(); 45 | for (ApkVerifier.IssueWithParams issue : result.getWarnings()) { 46 | messages.add(issue.toString()); 47 | } 48 | this.warnings = messages.toArray(warnings); 49 | messages.clear(); 50 | for (ApkVerifier.IssueWithParams issue : result.getErrors()) { 51 | messages.add(issue.toString()); 52 | } 53 | this.errors = messages.toArray(errors); 54 | } 55 | 56 | } 57 | 58 | private final TaskListener listener; 59 | 60 | VerifyApkCallable(TaskListener listener) { 61 | this.listener = listener; 62 | } 63 | 64 | @Override 65 | public VerifyResult invoke(File inputApkFile, VirtualChannel channel) throws IOException, InterruptedException { 66 | 67 | ApkVerifier verifier = new ApkVerifier.Builder(inputApkFile).build(); 68 | try { 69 | ApkVerifier.Result result = verifier.verify(); 70 | return new VerifyResult(result); 71 | } 72 | catch (Exception e) { 73 | e.printStackTrace(listener.getLogger()); 74 | throw new IOException(e.getMessage()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/androidsigning/compatibility/config-2.0.8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | 17 | 18 | android-signing-1 19 | key1 20 | build/outputs/apk/*-unsigned.apk 21 | true 22 | true 23 | 24 | 25 | android-signing-1 26 | key2 27 | SignApksBuilderTest.apk, SignApksBuilderTest-choc*.apk 28 | false 29 | true 30 | 31 | 32 | android-signing-2 33 | key1 34 | **/*.apk 35 | false 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | android-signing-1 44 | key1 45 | build/outputs/apk/*-unsigned.apk 46 | true 47 | true 48 | 49 | 50 | android-signing-1 51 | key2 52 | SignApksBuilderTest.apk, SignApksBuilderTest-choc*.apk 53 | false 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_1_0_Test.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning.compatibility; 2 | 3 | import org.jenkinsci.plugins.androidsigning.SignApksBuilder; 4 | import org.jenkinsci.plugins.androidsigning.SignedApkMappingStrategy; 5 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.jvnet.hudson.test.JenkinsRule; 9 | import org.jvnet.hudson.test.recipes.LocalData; 10 | 11 | import java.io.IOException; 12 | import java.net.URISyntaxException; 13 | 14 | import hudson.model.FreeStyleProject; 15 | import hudson.tasks.Builder; 16 | import hudson.util.DescribableList; 17 | 18 | import static org.hamcrest.CoreMatchers.equalTo; 19 | import static org.hamcrest.CoreMatchers.instanceOf; 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | 23 | 24 | public class SignApksBuilderCompatibility_2_1_0_Test { 25 | 26 | @Rule 27 | public JenkinsRule testJenkins = new JenkinsRule(); 28 | 29 | @Test 30 | @LocalData 31 | public void doesNotSkipZipalignFor_v2_1_0_builders() throws Exception { 32 | 33 | FreeStyleProject job = (FreeStyleProject) testJenkins.jenkins.getItem(getClass().getSimpleName()); 34 | DescribableList builders = job.getBuildersList(); 35 | 36 | assertThat(builders.size(), equalTo(2)); 37 | 38 | SignApksBuilder builder = (SignApksBuilder) builders.get(0); 39 | assertThat(builder.getKeyStoreId(), equalTo("android-team-1")); 40 | assertThat(builder.getKeyAlias(), equalTo("android-team-1")); 41 | assertThat(builder.getApksToSign(), equalTo("build/outputs/apk/*-unsigned.apk")); 42 | assertThat(builder.getArchiveSignedApks(), is(true)); 43 | assertThat(builder.getArchiveUnsignedApks(), is(true)); 44 | assertThat(builder.getSkipZipalign(), is(false)); 45 | 46 | builder = (SignApksBuilder) builders.get(1); 47 | assertThat(builder.getKeyStoreId(), equalTo("android-team-2")); 48 | assertThat(builder.getKeyAlias(), equalTo("android-team-2")); 49 | assertThat(builder.getApksToSign(), equalTo("**/*-unsigned.apk")); 50 | assertThat(builder.getArchiveSignedApks(), is(true)); 51 | assertThat(builder.getArchiveUnsignedApks(), is(false)); 52 | assertThat(builder.getSkipZipalign(), is(false)); 53 | } 54 | 55 | @Test 56 | @LocalData 57 | public void usesOldSignedApkMappingFor_v2_1_0_builders() throws Exception { 58 | 59 | FreeStyleProject job = (FreeStyleProject) testJenkins.jenkins.getItem(getClass().getSimpleName()); 60 | DescribableList builders = job.getBuildersList(); 61 | 62 | assertThat(builders.size(), equalTo(2)); 63 | 64 | SignApksBuilder builder = (SignApksBuilder) builders.get(0); 65 | assertThat(builder.getSignedApkMapping(), instanceOf(SignedApkMappingStrategy.UnsignedApkBuilderDirMapping.class)); 66 | 67 | builder = (SignApksBuilder) builders.get(1); 68 | assertThat(builder.getSignedApkMapping(), instanceOf(SignedApkMappingStrategy.UnsignedApkBuilderDirMapping.class)); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/SignApksDslContext.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | 4 | import hudson.Extension; 5 | import javaposse.jobdsl.dsl.Context; 6 | import javaposse.jobdsl.dsl.helpers.step.StepContext; 7 | import javaposse.jobdsl.plugin.ContextExtensionPoint; 8 | import javaposse.jobdsl.plugin.DslExtensionMethod; 9 | 10 | @Extension(optional = true) 11 | public class SignApksDslContext extends ContextExtensionPoint { 12 | 13 | public static class ConfigureContext implements Context { 14 | 15 | private final SignApksBuilder builder; 16 | 17 | private ConfigureContext(SignApksBuilder builder) { 18 | this.builder = builder; 19 | } 20 | 21 | public void keyStoreId(String x) { 22 | builder.setKeyStoreId(x); 23 | } 24 | 25 | public void keyAlias(String x) { 26 | builder.setKeyAlias(x); 27 | } 28 | 29 | public void signedApkMapping(SignedApkMappingStrategy x) { 30 | builder.setSignedApkMapping(x); 31 | } 32 | 33 | public void signedApkMapping(Runnable configClosure) { 34 | SignedApkMappingContext context = new SignedApkMappingContext(builder); 35 | executeInContext(configClosure, context); 36 | } 37 | 38 | public void skipZipalign(boolean x) { 39 | builder.setSkipZipalign(x); 40 | } 41 | 42 | public void archiveSignedApks(boolean x) { 43 | builder.setArchiveSignedApks(x); 44 | } 45 | 46 | public void archiveUnsignedApks(boolean x) { 47 | builder.setArchiveUnsignedApks(x); 48 | } 49 | 50 | public void androidHome(String x) { 51 | builder.setAndroidHome(x); 52 | } 53 | 54 | public void zipalignPath(String x) { 55 | builder.setZipalignPath(x); 56 | } 57 | 58 | public SignedApkMappingStrategy.UnsignedApkSiblingMapping unsignedApkSibling() { 59 | return new SignedApkMappingStrategy.UnsignedApkSiblingMapping(); 60 | } 61 | 62 | public SignedApkMappingStrategy.UnsignedApkBuilderDirMapping unsignedApkNameDir() { 63 | return new SignedApkMappingStrategy.UnsignedApkBuilderDirMapping(); 64 | } 65 | } 66 | 67 | public static class SignedApkMappingContext implements Context { 68 | private final SignApksBuilder builder; 69 | SignedApkMappingContext(SignApksBuilder builder) { 70 | this.builder = builder; 71 | } 72 | 73 | public void unsignedApkSibling() { 74 | builder.setSignedApkMapping(new SignedApkMappingStrategy.UnsignedApkSiblingMapping()); 75 | } 76 | 77 | public void unsignedApkNameDir() { 78 | builder.setSignedApkMapping(new SignedApkMappingStrategy.UnsignedApkBuilderDirMapping()); 79 | } 80 | } 81 | 82 | @DslExtensionMethod(context = StepContext.class) 83 | public SignApksBuilder signAndroidApks(String apksToSign, Runnable configClosure) { 84 | SignApksBuilder builder = new SignApksBuilder(); 85 | builder.setApksToSign(apksToSign); 86 | executeInContext(configClosure, new ConfigureContext(builder)); 87 | return builder; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-pair-exposed.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAytDUAB0SN+S5hqFd+6wKNJqUq/R3ofbtzDUmdy7raigtfvqT 3 | 46iE+gRU6ztaHgO54jgfQ8ySyIh8Pk29PiO2fQjsOyvWRqCoKkbW6WfGxc/1mdTu 4 | Hj6ukevnK+HRAsDiF5IVHGO8sDPxbnoIhpE/uPW29n+vLyqS4iAOKpqbOtiGyAMz 5 | Jn/Cn5NddmlM2xhNNkhiNEBwY6zlI4sltw2jYYUXuEu6IU8S4dAlobHLxDbj4ZG4 6 | O0XmPjSomIl3n6jF3HhdA6uiU2AJd4z6Vq63uUB21/wet7zx1rGKht5yNFduDdok 7 | JTgGrmYc1GKD0l0XP2X7Zyeyl3jZpSl61mSNUwIDAQABAoIBAQC5knv4IqFxzPFI 8 | U0wIJDEuUqZn9Aamhqasq5EniiHS/zIptiMtMhuCHAaLOcJKJkSlzY4l3gAMRn3Q 9 | EBdwzQKDw29K8OBdvVBBZpHr/I1un8psV90MhXP7hmr9xuAUceItiPeSA1w5qT1m 10 | RXiZkDtLBGDFiK0FLiI5jvUHXHFeU+7Ty7NFkt1sE7U+0gLKn5e8ClVzqSE+r3DX 11 | aSjdWsjH6PXCjWRTGGsr63CWFVc75mJaL3GZsI55XQQyUgNdlzv3DRfMb6NQjNJy 12 | UEjEgwtl79mW1H9qmQmDL9LnOMesP+Ka7CQhcMLHVECIJKc52DGcudkmdQxUOKZR 13 | DaIYL70RAoGBAP8P6k1mIABvUoU0GKYXNkRR+YLuFOgG/aPxbE5o8jtMXDZV2Vf0 14 | uSwYbSSsqBHyAzVWoF7Z5DgHWbXhS9xk7xmM+Kro+F2iA9sVMlHFeW1s9IXmfy+6 15 | oxmK3WuuP3PB/0y/Mdkyu8U57Y6Heb2VEEEwhors6TrkdYiXEt7uCp9JAoGBAMuP 16 | vAEs5CQPJTsAnJNlxrpJEG3rYkoN8yqKPXmvP7t00d4sBd/2eZ8Y+EFtgWBM1K3e 17 | vACvDuaffGalx2Yl/MgoM3L/57l6pEdamj0ZwPF6cfbdsiJ9yvI2mu0cPMG+TqhP 18 | xukytx4ttIrZaSRfSyxrUcbF4CQ2+hn62yFnqZu7AoGBANW8+YRIq1KR9x3mrS0p 19 | 0HDqHOPqLRzPFue2XSNL8IlekPt7b3m0eyQHiBaulN0M6EFfSV6SyxtklXnDxXV4 20 | I6FOr+dQ+ShFp2OE3LkHeZ0IK9S1dimCBkFWS/x7dXLEw/MFWXmAeTdqNrc0sgD7 21 | lDZ8upJau4t9fTysFMU5xy75AoGBAMZ/o+I1h0bOagyuHQDy1yXih53YUaFLFxsd 22 | cLMPPIOsd9ZBcX0i2RhWfgc3JFjmsuHVd9jm3A3x6ZojAF1Qn74CzaDPgIRy3m0i 23 | IZOBYI9ZSnZjWwidR+CHdO3QgkKfNA6WtK3EIaLRCOP4+7lXH3PyNu0xGc/WuG3L 24 | HBHoBxFvAoGABjwtoo/z2vVX+jc9scnsJdCz71+m929jYcW4sk1O0IoNSRb63moz 25 | Yj5YovLYgPvjmB3SI3F0fx6i9TR4hqmFbc9aLr0aL8Eby4kOjwpG3XxNpXQgSAAi 26 | h9+0NgaQCpPZjMstzJu513+rYoyd8RUQdaTodNqTgQxgTIZG4hHvpVM= 27 | -----END RSA PRIVATE KEY----- 28 | -----BEGIN CERTIFICATE----- 29 | MIIEUTCCAzmgAwIBAgIJAKT/DjoEUfZEMA0GCSqGSIb3DQEBBQUAMHgxHDAaBgNV 30 | BAMTE1NpZ25BcGtzQnVpbGRlclRlc3QxGDAWBgNVBAsTD2FuZHJvaWQtc2lnbmlu 31 | ZzEfMB0GA1UEChMWb3JnLmplbmtpbnMtY2kucGx1Z2luczEQMA4GA1UEBxMHSmVu 32 | a2luczELMAkGA1UEBhMCSU8wHhcNMTcwMjAxMjMwMTAyWhcNNDQwNjE4MjMwMTAy 33 | WjB4MRwwGgYDVQQDExNTaWduQXBrc0J1aWxkZXJUZXN0MRgwFgYDVQQLEw9hbmRy 34 | b2lkLXNpZ25pbmcxHzAdBgNVBAoTFm9yZy5qZW5raW5zLWNpLnBsdWdpbnMxEDAO 35 | BgNVBAcTB0plbmtpbnMxCzAJBgNVBAYTAklPMIIBIjANBgkqhkiG9w0BAQEFAAOC 36 | AQ8AMIIBCgKCAQEAytDUAB0SN+S5hqFd+6wKNJqUq/R3ofbtzDUmdy7raigtfvqT 37 | 46iE+gRU6ztaHgO54jgfQ8ySyIh8Pk29PiO2fQjsOyvWRqCoKkbW6WfGxc/1mdTu 38 | Hj6ukevnK+HRAsDiF5IVHGO8sDPxbnoIhpE/uPW29n+vLyqS4iAOKpqbOtiGyAMz 39 | Jn/Cn5NddmlM2xhNNkhiNEBwY6zlI4sltw2jYYUXuEu6IU8S4dAlobHLxDbj4ZG4 40 | O0XmPjSomIl3n6jF3HhdA6uiU2AJd4z6Vq63uUB21/wet7zx1rGKht5yNFduDdok 41 | JTgGrmYc1GKD0l0XP2X7Zyeyl3jZpSl61mSNUwIDAQABo4HdMIHaMB0GA1UdDgQW 42 | BBS25sctiYe1nG32+NCPX7wNou6XOzCBqgYDVR0jBIGiMIGfgBS25sctiYe1nG32 43 | +NCPX7wNou6XO6F8pHoweDEcMBoGA1UEAxMTU2lnbkFwa3NCdWlsZGVyVGVzdDEY 44 | MBYGA1UECxMPYW5kcm9pZC1zaWduaW5nMR8wHQYDVQQKExZvcmcuamVua2lucy1j 45 | aS5wbHVnaW5zMRAwDgYDVQQHEwdKZW5raW5zMQswCQYDVQQGEwJJT4IJAKT/DjoE 46 | UfZEMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAF99HJstuOqIi3KN 47 | vvQEnJ00Z0iyglIZQhw43uKVkkUyKZhiQFgwIDvQEMJEVBHa9gLg0QwistBZOv9h 48 | Atqyt/vlAY5Y6iXtqLY9sVT0bHfGnF4FRoi9NVxpJp+EqZeC6WdKesnU6uU2fXdS 49 | LtczC37GsvUlLD3rhj5BbvDTW1CzkSezOjDJJkwjqm4eh9v95lf0XlGj+9A1OWEw 50 | /SfkpDw3F87xR5djY05WITS3zSDdXLPAZ37KNOnra/avu9kKQrakHDD1bVekxm4n 51 | ML/VWDol5jqYCjn23kNUyxTU/y42dCrCVUkYUM9GliAW00vUxWIFpMrSbAHd+/vv 52 | cDDXd9k= 53 | -----END CERTIFICATE----- 54 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest2-pair.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,A8A85BE30D2A0C6C 4 | 5 | m2QjVKbNDKl1JKYhXn1PXMTKvxOPXlN1cG7U3hdaeG99eGxX20FaMZ0TZUsCoPmh 6 | 1RSsHBuRrRxjZsV3V+QJMWCRibZX9j/r9sYEXkfUblwF8+Ye9ZkZJqLWbLozDaCe 7 | 4hErZhmNflHMekrNmk0Z8qJvW3v5wC8oontknM6WWNqvWiQia4D5l5TqHFFzjzej 8 | Bg2OLGlNEhPG0oAlNzRHxXiNO8f3VExVidNEOOKsE93HseGfnBBDDVvucMoHYq8O 9 | wGUv3mbYU9K3ZQlzVGf/lYMXOws1pkmttyyffbF/N3ZEI/p9Em75szjXems02yX3 10 | ley1k9qM1ijsA9aa7kW+YXTWnrsM0ixvLjKN5BEVz6yiWtINpah++ABUZh7wi82S 11 | Aqt6DIM4R+PQRdDSue0HDDJRg5wHKoqDfIOrsRgC2AsE2VMh3nR+Rp/Gxd0qOrjy 12 | pypiTJY1jjWlolF4nI05ut+MnVtGWnqewNjRtkl8gm1efgnBu3pnBZIo5d8gB/2P 13 | VhzjUV/zRoA6v9xD1UXzJ8C43SaTLOBz85uXeOGyny7paxIlFHupby2VAnAks4Oh 14 | AGDIGeG2KGLTPMWp9H6Nmjt+um+gGjXN8ilbM7v3oaTbOcSKZXc9Jq1iVWPH5Y2b 15 | 5CXAFY0TYYYEUxGZGIFDN2sqMqnvv1xkMIO41RmQs5QYgGkmEvPPfxt31EevNj8e 16 | r47bnVB80AEA+1/Wye+JjhpfsV7J17h2znZtOewaXZPgGEEyWhGZY91d/OBJMB+l 17 | bdlcYTnsQdUYf2FRRPRN4lKoERP2Z/faE3/1ZxbW3tu46f8lAFL6PXzb2DwrRSCt 18 | oLbmy6kHeMeNjx9QGeNHVlY7YtaBEEeKZMI5E64PlBtjvD1hQlPr+8lxp+//0h2+ 19 | SGIjcqFMC4DZpSzI69TecMS0tADGlK3OUWnz6cZwBvorvNDLXKeUzsol9Js/YRn4 20 | unx9cQUfa/38iHc++oleHzxu5UT8+6GmpJFV/2KmgR21k5BLQityAC7g+WD/QeUn 21 | oWxZgrvnL1yUw7dwMHH76hVGEsAzlpSkA/fgaFWJVltEGRMTSZdY27RrkfeF3azU 22 | AsrsQHLfB5lq1N9b3nkaVM2ODegBpkcSMg9J2Q1OZ6Piq3vlYWQ+qWZ43g7J6u91 23 | UEe7J+YuQvqW6bkRymcL4NK0Bd9kUu8IOPEcgXfyAGu67OLka2w0RHHNDeIq8r5G 24 | WHOezFe+koaAX2FY+39O4FeoARBuMYa0Xrgc6XCarTts94mHacKgnJG7T5MWVPqA 25 | 5TSuOc0SoS3MfOSaJAXzXRB5hqpjy5lokZjVfNgKKcrso26JGez4kUmK+3DI7PH7 26 | d+bqLhnUBUr55+WKrt3Jn4zXlmnvnCGE0vOeQor+w6CNwSuSrE+Ajcmqq4KmmIxd 27 | NiFGY8wF5e9Xk5cTNtB5vR1wliqKu3Vb8ldVV0hMtEJ0xiTtw26p4uM/1jNMTEnk 28 | iVf2r5ooJ1Gy4W9zgM1ohIdLODojo4PaYzOKspkntwnfKn8QkDFUsFoqYN56x9zJ 29 | ZHnxZCD0d6Ugp9bk4l4wt/kPcg6jjLHxbVpr0PDjZojvyWDOxQaAKlExJLU+eX4C 30 | -----END RSA PRIVATE KEY----- 31 | -----BEGIN CERTIFICATE----- 32 | MIIEJzCCAw+gAwIBAgIJAO9DVq8SxH/aMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV 33 | BAYTAklPMRAwDgYDVQQIEwdKZW5raW5zMRAwDgYDVQQKEwdQbHVnaW5zMRgwFgYD 34 | VQQLEw9BbmRyb2lkIFNpZ25pbmcxHTAbBgNVBAMTFFNpZ25BcGtzQnVpbGRlclRl 35 | c3QyMB4XDTE3MDUyOTE0MjQ0NloXDTQ0MTAxMzE0MjQ0NlowajELMAkGA1UEBhMC 36 | SU8xEDAOBgNVBAgTB0plbmtpbnMxEDAOBgNVBAoTB1BsdWdpbnMxGDAWBgNVBAsT 37 | D0FuZHJvaWQgU2lnbmluZzEdMBsGA1UEAxMUU2lnbkFwa3NCdWlsZGVyVGVzdDIw 38 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3w0oFnoP3vk24zTLpeKf7 39 | S35iEUAmPeRl47z/xIFfoVUPVTv79/EwGroEHM3nGNwUjyqJRT+wdqugGIzFrGKP 40 | A13L7dJl4O2LQaqPlPkJoV9PTUrz0QltJrR6N4AwjIZ9hOfssK5JTPWRk3s3Q5fW 41 | zIYHXQwhZboP73lZ4I5lL6GaHpXullHq7G7ksGeEOYKoZQurKBUbxSd2WxKatHO2 42 | cNNg+4U9uC0iWGP5wXTtgxwiLE0yLmFR/R3h7nCqGtC4K+kJby+c9WXzkiUarr7G 43 | uD5N/ZAHsuhwJtST/w3A0R5/FCRxEGlmObAidxqyCngRnVDHAmcCnZBHuqB3P1bJ 44 | AgMBAAGjgc8wgcwwHQYDVR0OBBYEFADHFRIN+PQWzXFXKsM8idgQVla5MIGcBgNV 45 | HSMEgZQwgZGAFADHFRIN+PQWzXFXKsM8idgQVla5oW6kbDBqMQswCQYDVQQGEwJJ 46 | TzEQMA4GA1UECBMHSmVua2luczEQMA4GA1UEChMHUGx1Z2luczEYMBYGA1UECxMP 47 | QW5kcm9pZCBTaWduaW5nMR0wGwYDVQQDExRTaWduQXBrc0J1aWxkZXJUZXN0MoIJ 48 | AO9DVq8SxH/aMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAG2HqRQ1 49 | zvIW0liyyE7fYnIZDRnL9A3xLYXoy+kfkZngtLZt2vflmR0Zf9CSyEngCI1o8+bh 50 | HXQuQr5rxhQC6tCWt5LdQTtJRLKLSsi0YuvmUnou8O0IF7WfylZF2fRryAhxKWC/ 51 | MBIdP9MiZLGcxWdWAZDMG4r/ZCWOCfegY1Sy2xnTOfVrznI8HBdfblK/ePYHxccB 52 | Vb/EpJ79T6gySV6G9OxYz4l7DnmOucJ94DQi29vT5249srXO6sI9VYcTX5YVpSmW 53 | l0NNHcfXB8PszGJ+IR+LHhgS1FvIdv9H9bw2KELHlkhX82IT87wFG2xpqUkL77jb 54 | dacH2XrJeqvOxHY= 55 | -----END CERTIFICATE----- 56 | -------------------------------------------------------------------------------- /src/test/resources/SignApksBuilderTest-pair.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,330C635939CB5235 4 | 5 | DKCPOdNDM2F0riXPs2o0aPeggN44vYEWUwlSjVzN8jP1NyRPCxbHkWX3m/+F7LvW 6 | abNOXSl7Bv0DCWikmyJupWNQhskQcuB2W4jobvvSojLwOJbBy5rYN5vHUTOc1JlX 7 | wyeWMUYDv/2/otxfMSoNMIX+uVP2WuIxpT/JBKt4KLiPAq0IJ8kk3nFcBOMScOv/ 8 | o80fS7RuWvgqsw3Gap829aTozLMeoHX6cIN+yEuDEgvSDSy5nbKRnqtlvBXLmYGe 9 | ZH7MPVLIqKxjub/ccAAhy7WIav91yjboSqZc5l7qGN/MTg8oxhuzw5Uq1bEYY70E 10 | ZlANzmn8bcxz0hUKCS4lauOAxoNScTRfD2Eq5qt/ltzcR0FSt4WCrRK8gA8WwLXg 11 | 8X2oyJYXQRciAev3I9fwDGIdGI45SNa0vZu+qP7Qrq7GRtnl5Lbsd+KCKhPZWqV5 12 | 00nKN0Qtpd7bpOsq73xCNV+qjhp8PfabnlozSEx9NxA+arW/1L71MIJmcDsiqku8 13 | F+Nc9L3QKXFFjMYswAoqfYo5yHtdchkS22iKnjLeJa3yYm8doFbIniQz8VXr2aUg 14 | Lguf3rNB9jKWpG0263E9juWeXuTWbcuMDIwZ4eCemUF/ZixQWt3QrHIBAVGPWu2j 15 | A1FgGMl0Jpitd4h5OMKA7ok/m9ycVOY1137qiebmngH5xD+QnW9Fe8/IsC1S4CEr 16 | QYOulkzrh7jzEZjnSQr35ftY7BhN0CA1U2KzoSV8zUMkENPiIlu8TvLXZ6/eb8lp 17 | Ds4FJLTvXkIZHjcDHLNVnolDH/Y5ZfeAl0pGPtv40dUkX2DHKpkobD2pW15NiR6K 18 | qr4iR8HJltYf+TDk+7acro8YMvExtgZsHm4j6dXnE7sF6E7xKdFOD+37HDOYL+r3 19 | xF7+xNwEm7KC4LxhqfncceYOt1n6i5mXSzVXHCRA+zOnDWJPzMNJ7ntEEG8iIr9s 20 | c9q5Zk623glC9qeMsk74XmkYvg8oBuPq009o8TBz1IDZ+hpLGp+GdWTy87Z9IXiq 21 | 9P5RFcmCiFP1eBKGUnv/OiHf1oulIwH7DmgeVbg83+u2PM4FxvnrilSLTk8Faxz+ 22 | ih62d1P7L4IbOnflM3Mu4bOwLUb+zGWOBQ2sd2ftD90icNVwKL2n11GKQ8Fonzkz 23 | 8QbNaZFWmcOG7JxkfyMMarK7go3qqipji/2yvATsDkDaU7EeLoFDSs/vuQ5DEZWO 24 | 9I22Myvq+gC9CwbajASYSLObOyOq1DklmWYpkU8iBEoN8tN1bjDndKiMC6zSCq/d 25 | boSRNsgBGu5lpR7DeVzANj4PCiDqmJp73qWhhFkz8yOgc/J6pdocFwfWPldw73Mh 26 | MNH4wIXu0nBpAqmZ7C0ye5FbykBuALOA0IPkO7ocykbNw+dBTvdHTx/lMYEX3gRc 27 | cTNYOCEZ7WBQHAkF7L3BtlpW23n46Jq9bjC/LxG12tQe3fXoTkXBgVcLjUeaqjQN 28 | TNu3tWHSFtou2z1xweatNi2E2mJx7gnFiJIYLJjfzdS3+FEPtqhpPC7/NPKCpU5/ 29 | I5wMOoIWjIPkxBM5Tp026wwdKZ0CudtWUZ0JDKAFQGXb1PPcv+kaToUAeW98/pTZ 30 | -----END RSA PRIVATE KEY----- 31 | -----BEGIN CERTIFICATE----- 32 | MIIEUTCCAzmgAwIBAgIJAKT/DjoEUfZEMA0GCSqGSIb3DQEBBQUAMHgxHDAaBgNV 33 | BAMTE1NpZ25BcGtzQnVpbGRlclRlc3QxGDAWBgNVBAsTD2FuZHJvaWQtc2lnbmlu 34 | ZzEfMB0GA1UEChMWb3JnLmplbmtpbnMtY2kucGx1Z2luczEQMA4GA1UEBxMHSmVu 35 | a2luczELMAkGA1UEBhMCSU8wHhcNMTcwMjAxMjMwMTAyWhcNNDQwNjE4MjMwMTAy 36 | WjB4MRwwGgYDVQQDExNTaWduQXBrc0J1aWxkZXJUZXN0MRgwFgYDVQQLEw9hbmRy 37 | b2lkLXNpZ25pbmcxHzAdBgNVBAoTFm9yZy5qZW5raW5zLWNpLnBsdWdpbnMxEDAO 38 | BgNVBAcTB0plbmtpbnMxCzAJBgNVBAYTAklPMIIBIjANBgkqhkiG9w0BAQEFAAOC 39 | AQ8AMIIBCgKCAQEAytDUAB0SN+S5hqFd+6wKNJqUq/R3ofbtzDUmdy7raigtfvqT 40 | 46iE+gRU6ztaHgO54jgfQ8ySyIh8Pk29PiO2fQjsOyvWRqCoKkbW6WfGxc/1mdTu 41 | Hj6ukevnK+HRAsDiF5IVHGO8sDPxbnoIhpE/uPW29n+vLyqS4iAOKpqbOtiGyAMz 42 | Jn/Cn5NddmlM2xhNNkhiNEBwY6zlI4sltw2jYYUXuEu6IU8S4dAlobHLxDbj4ZG4 43 | O0XmPjSomIl3n6jF3HhdA6uiU2AJd4z6Vq63uUB21/wet7zx1rGKht5yNFduDdok 44 | JTgGrmYc1GKD0l0XP2X7Zyeyl3jZpSl61mSNUwIDAQABo4HdMIHaMB0GA1UdDgQW 45 | BBS25sctiYe1nG32+NCPX7wNou6XOzCBqgYDVR0jBIGiMIGfgBS25sctiYe1nG32 46 | +NCPX7wNou6XO6F8pHoweDEcMBoGA1UEAxMTU2lnbkFwa3NCdWlsZGVyVGVzdDEY 47 | MBYGA1UECxMPYW5kcm9pZC1zaWduaW5nMR8wHQYDVQQKExZvcmcuamVua2lucy1j 48 | aS5wbHVnaW5zMRAwDgYDVQQHEwdKZW5raW5zMQswCQYDVQQGEwJJT4IJAKT/DjoE 49 | UfZEMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAF99HJstuOqIi3KN 50 | vvQEnJ00Z0iyglIZQhw43uKVkkUyKZhiQFgwIDvQEMJEVBHa9gLg0QwistBZOv9h 51 | Atqyt/vlAY5Y6iXtqLY9sVT0bHfGnF4FRoi9NVxpJp+EqZeC6WdKesnU6uU2fXdS 52 | LtczC37GsvUlLD3rhj5BbvDTW1CzkSezOjDJJkwjqm4eh9v95lf0XlGj+9A1OWEw 53 | /SfkpDw3F87xR5djY05WITS3zSDdXLPAZ37KNOnra/avu9kKQrakHDD1bVekxm4n 54 | ML/VWDol5jqYCjn23kNUyxTU/y42dCrCVUkYUM9GliAW00vUxWIFpMrSbAHd+/vv 55 | cDDXd9k= 56 | -----END CERTIFICATE----- 57 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/ApkArtifactIsSignedMatcher.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; 6 | 7 | import org.hamcrest.BaseMatcher; 8 | import org.hamcrest.Description; 9 | 10 | import java.security.KeyStoreException; 11 | import java.security.cert.X509Certificate; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import hudson.FilePath; 16 | import hudson.model.TaskListener; 17 | import hudson.security.ACL; 18 | import jenkins.model.Jenkins; 19 | import jenkins.util.VirtualFile; 20 | 21 | 22 | class ApkArtifactIsSignedMatcher extends BaseMatcher { 23 | private final StandardCertificateCredentials signer; 24 | private final X509Certificate expectedCert; 25 | private final StringBuilder descText = new StringBuilder(); 26 | 27 | private ApkArtifactIsSignedMatcher(String keyStoreId, String keyAlias) throws KeyStoreException { 28 | List result = CredentialsProvider.lookupCredentialsInItemGroup( 29 | StandardCertificateCredentials.class, Jenkins.get(), ACL.SYSTEM2, Collections.emptyList()); 30 | signer = CredentialsMatchers.firstOrNull(result, CredentialsMatchers.withId(keyStoreId)); 31 | expectedCert = (X509Certificate) signer.getKeyStore().getCertificate(keyAlias); 32 | } 33 | 34 | static ApkArtifactIsSignedMatcher isSignedWith(String keyStoreId, String keyAlias) throws KeyStoreException { 35 | return new ApkArtifactIsSignedMatcher(keyStoreId, keyAlias); 36 | } 37 | 38 | @Override 39 | public boolean matches(Object item) { 40 | BuildArtifact actual = (BuildArtifact) item; 41 | descText.append(actual.artifact.getFileName()); 42 | try { 43 | VirtualFile virtualSignedApk = actual.build.getArtifactManager().root().child(actual.artifact.relativePath); 44 | FilePath signedApkPath = actual.build.getWorkspace().createTempFile(actual.artifact.getFileName().replace(".apk", ""), ".apk"); 45 | signedApkPath.copyFrom(virtualSignedApk.open()); 46 | VerifyApkCallable.VerifyResult result = signedApkPath.act(new VerifyApkCallable(TaskListener.NULL)); 47 | if (!result.isVerified) { 48 | descText.append(" not verified;"); 49 | } 50 | if (!result.isVerifiedV3Scheme) { 51 | descText.append(" not verified v3;"); 52 | } 53 | if (!result.isVerifiedV2Scheme) { 54 | descText.append(" not verified v2;"); 55 | } 56 | if (!result.isVerifiedV1Scheme) { 57 | descText.append(" not verified v1;"); 58 | } 59 | if (result.certs.length != 1) { 60 | descText.append(" signer cert chain length should be 1, was ").append(result.certs.length); 61 | } 62 | else if (!result.certs[0].equals(expectedCert)) { 63 | descText.append(" signer cert differs from expected cert"); 64 | } 65 | } 66 | catch (Exception e) { 67 | throw new RuntimeException(e); 68 | } 69 | 70 | return descText.length() == actual.artifact.getFileName().length(); 71 | } 72 | 73 | @Override 74 | public void describeTo(Description description) { 75 | description.appendText(descText.toString()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/SignedApkMappingStrategy.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.jenkinsci.Symbol; 4 | import org.kohsuke.stapler.DataBoundConstructor; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import edu.umd.cs.findbugs.annotations.NonNull; 10 | 11 | import hudson.Extension; 12 | import hudson.ExtensionList; 13 | import hudson.ExtensionPoint; 14 | import hudson.FilePath; 15 | import hudson.model.AbstractDescribableImpl; 16 | import hudson.model.Descriptor; 17 | import jenkins.model.Jenkins; 18 | 19 | 20 | public abstract class SignedApkMappingStrategy extends AbstractDescribableImpl implements ExtensionPoint { 21 | 22 | public abstract FilePath destinationForUnsignedApk(FilePath unsignedApk, FilePath workspace); 23 | 24 | public static ExtensionList all() { 25 | return Jenkins.getActiveInstance().getExtensionList(SignedApkMappingStrategy.class); 26 | } 27 | 28 | /** 29 | * Return the name of the given APK without the .apk extension and without any -unsigned suffix, if present. 30 | * For example, {@code}myApp-unsigned.apk{@code} returns {@code}myApp{@code}, and 31 | * {@code}myApp-someFlavor.apk{@code} returns {@code}myApp-someFlavor{@code}. 32 | * @param unsignedApk 33 | * @return 34 | */ 35 | public static String unqualifiedNameOfUnsignedApk(FilePath unsignedApk) { 36 | Pattern stripUnsignedPattern = Pattern.compile("(-?unsigned)?$", Pattern.CASE_INSENSITIVE); 37 | Matcher stripUnsigned = stripUnsignedPattern.matcher(unsignedApk.getBaseName()); 38 | return stripUnsigned.replaceFirst(""); 39 | } 40 | 41 | public static class UnsignedApkBuilderDirMapping extends SignedApkMappingStrategy { 42 | 43 | @DataBoundConstructor 44 | public UnsignedApkBuilderDirMapping() { 45 | } 46 | 47 | @Override 48 | public FilePath destinationForUnsignedApk(FilePath unsignedApk, FilePath workspace) { 49 | String strippedName = unqualifiedNameOfUnsignedApk(unsignedApk); 50 | return workspace.child(SignApksBuilder.BUILDER_DIR).child(unsignedApk.getName()).child(strippedName + "-signed.apk"); 51 | } 52 | 53 | @Extension 54 | @Symbol("unsignedApkNameDir") 55 | public static class DescriptorImpl extends Descriptor { 56 | @NonNull 57 | @Override 58 | public String getDisplayName() { 59 | return Messages.signedApkMapping_builderDir_displayName(); 60 | } 61 | } 62 | } 63 | 64 | public static class UnsignedApkSiblingMapping extends SignedApkMappingStrategy { 65 | 66 | @DataBoundConstructor 67 | public UnsignedApkSiblingMapping() { 68 | } 69 | 70 | @Override 71 | public FilePath destinationForUnsignedApk(FilePath unsignedApk, FilePath workspace) { 72 | String strippedName = unqualifiedNameOfUnsignedApk(unsignedApk); 73 | if (!unsignedApk.getBaseName().endsWith("-unsigned")) { 74 | strippedName += "-signed"; 75 | } 76 | FilePath file = unsignedApk.getParent(); 77 | if (file == null) { 78 | return null; 79 | } 80 | return file.child(strippedName + ".apk"); 81 | } 82 | 83 | @Extension 84 | @Symbol("unsignedApkSibling") 85 | public static class DescriptorImpl extends Descriptor { 86 | @NonNull 87 | @Override 88 | public String getDisplayName() { 89 | return Messages.signedApkMapping_unsignedSibling_displayName(); 90 | } 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/TestKeyStore.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsProvider; 4 | import com.cloudbees.plugins.credentials.CredentialsScope; 5 | import com.cloudbees.plugins.credentials.CredentialsStore; 6 | import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; 7 | import com.cloudbees.plugins.credentials.domains.Domain; 8 | import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; 9 | 10 | import org.junit.rules.TestRule; 11 | import org.junit.runner.Description; 12 | import org.junit.runners.model.Statement; 13 | import org.jvnet.hudson.test.JenkinsRule; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.util.Base64; 18 | 19 | 20 | public class TestKeyStore implements TestRule { 21 | 22 | public static final String KEY_STORE_RESOURCE = "/" + SignApksBuilderTest.class.getSimpleName() + ".p12"; 23 | public static final String KEY_STORE_ID = SignApksBuilderTest.class.getSimpleName() + ".keyStore"; 24 | public static final String KEY_ALIAS = SignApksBuilderTest.class.getSimpleName(); 25 | 26 | public final JenkinsRule testJenkins; 27 | public final String resourceName; 28 | public final String credentialsId; 29 | public final String description; 30 | public final String password; 31 | public StandardCertificateCredentials credentials; 32 | 33 | TestKeyStore(JenkinsRule testJenkins) { 34 | this(testJenkins, KEY_STORE_RESOURCE, KEY_STORE_ID, "Main Test Key Store", SignApksBuilderTest.class.getSimpleName()); 35 | } 36 | 37 | TestKeyStore(JenkinsRule testJenkins, String resourceName, String credentialsId, String description, String password) { 38 | this.testJenkins = testJenkins; 39 | this.resourceName = resourceName; 40 | this.credentialsId = credentialsId; 41 | this.description = description; 42 | this.password = password; 43 | } 44 | 45 | @Override 46 | public Statement apply(Statement base, Description description) { 47 | return new Statement() { 48 | @Override 49 | public void evaluate() throws Throwable { 50 | addCredentials(); 51 | try { 52 | base.evaluate(); 53 | } 54 | finally { 55 | removeCredentials(); 56 | } 57 | } 58 | }; 59 | } 60 | 61 | void addCredentials() { 62 | if (testJenkins.jenkins == null) { 63 | return; 64 | } 65 | try { 66 | InputStream keyStoreIn = SignApksBuilderTest.class.getResourceAsStream(resourceName); 67 | byte[] keyStoreBytes = new byte[keyStoreIn.available()]; 68 | keyStoreIn.read(keyStoreBytes); 69 | String keyStore = new String(Base64.getEncoder().encode(keyStoreBytes), "utf-8"); 70 | credentials = new CertificateCredentialsImpl( 71 | CredentialsScope.GLOBAL, credentialsId, description, password, 72 | new CertificateCredentialsImpl.UploadedKeyStoreSource(keyStore)); 73 | CredentialsStore store = CredentialsProvider.lookupStores(testJenkins.jenkins).iterator().next(); 74 | store.addCredentials(Domain.global(), credentials); 75 | } 76 | catch (Exception e) { 77 | throw new RuntimeException(e); 78 | } 79 | } 80 | 81 | void removeCredentials() { 82 | if (testJenkins.jenkins == null) { 83 | return; 84 | } 85 | CredentialsStore store = CredentialsProvider.lookupStores(testJenkins.jenkins).iterator().next(); 86 | try { 87 | store.removeCredentials(Domain.global(), credentials); 88 | } 89 | catch (IOException e) { 90 | throw new RuntimeException(e); 91 | } 92 | credentials = null; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/groovy/org/jenkinsci/plugins/androidsigning/SignApksDslContextTest.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning 2 | 3 | import hudson.model.FreeStyleProject 4 | import javaposse.jobdsl.plugin.ExecuteDslScripts 5 | import org.junit.Rule 6 | import org.junit.Test 7 | import org.jvnet.hudson.test.JenkinsRule 8 | 9 | import static org.hamcrest.CoreMatchers.equalTo 10 | import static org.hamcrest.CoreMatchers.instanceOf 11 | import static org.hamcrest.CoreMatchers.nullValue 12 | import static org.junit.Assert.* 13 | 14 | class SignApksDslContextTest { 15 | 16 | @Rule 17 | public JenkinsRule testJenkins = new JenkinsRule(); 18 | 19 | @Test 20 | void parsesSignApksDsl() { 21 | ExecuteDslScripts dslScripts = new ExecuteDslScripts( 22 | """ 23 | job('${this.class.simpleName}-generated') { 24 | steps { 25 | 26 | signAndroidApks '**/*-unsigned.apk', { 27 | keyStoreId 'my.keyStore' 28 | keyAlias 'myKey' 29 | archiveSignedApks true 30 | archiveUnsignedApks true 31 | androidHome '/fake/android-sdk' 32 | skipZipalign true 33 | } 34 | 35 | signAndroidApks '**/*-other.apk', { 36 | keyStoreId 'my.otherKeyStore' 37 | keyAlias 'myOtherKey' 38 | archiveSignedApks false 39 | archiveUnsignedApks false 40 | zipalignPath '/fake/android-sdk/zipalign' 41 | signedApkMapping unsignedApkNameDir() 42 | } 43 | 44 | signAndroidApks '**/*-other.apk', { 45 | keyStoreId 'my.otherKeyStore' 46 | keyAlias 'myOtherKey' 47 | archiveSignedApks false 48 | archiveUnsignedApks false 49 | zipalignPath '/fake/android-sdk/zipalign' 50 | signedApkMapping { unsignedApkSibling() } 51 | } 52 | } 53 | } 54 | """) 55 | FreeStyleProject job = testJenkins.createFreeStyleProject("${this.class.simpleName}-seed") 56 | job.buildersList.add(dslScripts) 57 | testJenkins.buildAndAssertSuccess(job) 58 | job = testJenkins.jenkins.getItemByFullName("${this.class.simpleName}-generated", FreeStyleProject) 59 | 60 | assertThat(job.builders.size(), equalTo(3)) 61 | 62 | SignApksBuilder signApks = job.builders[0] 63 | 64 | assertThat(signApks.apksToSign, equalTo("**/*-unsigned.apk")) 65 | assertThat(signApks.keyStoreId, equalTo("my.keyStore")) 66 | assertThat(signApks.keyAlias, equalTo("myKey")) 67 | assertTrue(signApks.skipZipalign) 68 | assertTrue(signApks.archiveSignedApks) 69 | assertTrue(signApks.archiveUnsignedApks) 70 | assertThat(signApks.androidHome, equalTo("/fake/android-sdk")) 71 | assertThat(signApks.zipalignPath, nullValue()) 72 | assertThat(signApks.signedApkMapping, instanceOf(SignedApkMappingStrategy.UnsignedApkSiblingMapping)) 73 | 74 | signApks = job.builders[1] 75 | 76 | assertThat(signApks.apksToSign, equalTo("**/*-other.apk")) 77 | assertThat(signApks.keyStoreId, equalTo("my.otherKeyStore")) 78 | assertThat(signApks.keyAlias, equalTo("myOtherKey")) 79 | assertFalse(signApks.skipZipalign) 80 | assertFalse(signApks.archiveSignedApks) 81 | assertFalse(signApks.archiveUnsignedApks) 82 | assertThat(signApks.androidHome, nullValue()) 83 | assertThat(signApks.zipalignPath, equalTo("/fake/android-sdk/zipalign")) 84 | assertThat(signApks.signedApkMapping, instanceOf(org.jenkinsci.plugins.androidsigning.SignedApkMappingStrategy.UnsignedApkBuilderDirMapping.class)) 85 | 86 | signApks = job.builders[2] 87 | 88 | assertThat(signApks.signedApkMapping, instanceOf(org.jenkinsci.plugins.androidsigning.SignedApkMappingStrategy.UnsignedApkSiblingMapping.class)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/compatibility/SignApksBuilderCompatibility_2_0_8_Test.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning.compatibility; 2 | 3 | import org.jenkinsci.plugins.androidsigning.SignApksBuilder; 4 | import org.jenkinsci.plugins.androidsigning.SignedApkMappingStrategy; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.jvnet.hudson.test.JenkinsRule; 8 | import org.jvnet.hudson.test.recipes.LocalData; 9 | 10 | import java.io.IOException; 11 | import java.net.URISyntaxException; 12 | 13 | import hudson.model.FreeStyleProject; 14 | import hudson.tasks.Builder; 15 | import hudson.util.DescribableList; 16 | 17 | import static org.hamcrest.CoreMatchers.equalTo; 18 | import static org.hamcrest.CoreMatchers.instanceOf; 19 | import static org.hamcrest.CoreMatchers.is; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | 22 | 23 | public class SignApksBuilderCompatibility_2_0_8_Test { 24 | 25 | @Rule 26 | public JenkinsRule testJenkins = new JenkinsRule(); 27 | 28 | @Test 29 | @LocalData 30 | public void converts_v2_0_8_entriesToBuilders() throws URISyntaxException, IOException { 31 | 32 | FreeStyleProject job = (FreeStyleProject) testJenkins.jenkins.getItem(getClass().getSimpleName()); 33 | DescribableList builders = job.getBuildersList(); 34 | 35 | assertThat(builders.size(), equalTo(3)); 36 | 37 | SignApksBuilder builder = (SignApksBuilder) builders.get(0); 38 | assertThat(builder.getKeyStoreId(), equalTo("android-signing-1")); 39 | assertThat(builder.getKeyAlias(), equalTo("key1")); 40 | assertThat(builder.getApksToSign(), equalTo("build/outputs/apk/*-unsigned.apk")); 41 | assertThat(builder.getArchiveUnsignedApks(), is(true)); 42 | assertThat(builder.getArchiveSignedApks(), is(true)); 43 | 44 | builder = (SignApksBuilder) builders.get(1); 45 | assertThat(builder.getKeyStoreId(), equalTo("android-signing-1")); 46 | assertThat(builder.getKeyAlias(), equalTo("key2")); 47 | assertThat(builder.getApksToSign(), equalTo("SignApksBuilderTest.apk, SignApksBuilderTest-choc*.apk")); 48 | assertThat(builder.getArchiveUnsignedApks(), is(false)); 49 | assertThat(builder.getArchiveSignedApks(), is(true)); 50 | 51 | builder = (SignApksBuilder) builders.get(2); 52 | assertThat(builder.getKeyStoreId(), equalTo("android-signing-2")); 53 | assertThat(builder.getKeyAlias(), equalTo("key1")); 54 | assertThat(builder.getApksToSign(), equalTo("**/*.apk")); 55 | assertThat(builder.getArchiveUnsignedApks(), is(false)); 56 | assertThat(builder.getArchiveSignedApks(), is(false)); 57 | } 58 | 59 | @Test 60 | @LocalData 61 | public void doesNotSkipZipalignFor_v2_0_8_builders() throws URISyntaxException, IOException { 62 | 63 | FreeStyleProject job = (FreeStyleProject) testJenkins.jenkins.getItem(getClass().getSimpleName()); 64 | DescribableList builders = job.getBuildersList(); 65 | 66 | assertThat(builders.size(), equalTo(3)); 67 | 68 | SignApksBuilder builder = (SignApksBuilder) builders.get(0); 69 | assertThat(builder.getSkipZipalign(), is(false)); 70 | 71 | builder = (SignApksBuilder) builders.get(1); 72 | assertThat(builder.getSkipZipalign(), is(false)); 73 | 74 | builder = (SignApksBuilder) builders.get(2); 75 | assertThat(builder.getSkipZipalign(), is(false)); 76 | } 77 | 78 | @Test 79 | @LocalData 80 | public void usesOldSignedApkMappingFor_v2_0_8_builders() throws Exception { 81 | 82 | FreeStyleProject job = (FreeStyleProject) testJenkins.jenkins.getItem(getClass().getSimpleName()); 83 | DescribableList builders = job.getBuildersList(); 84 | 85 | SignApksBuilder builder = (SignApksBuilder) builders.get(0); 86 | assertThat(builder.getSignedApkMapping(), instanceOf(SignedApkMappingStrategy.UnsignedApkBuilderDirMapping.class)); 87 | 88 | builder = (SignApksBuilder) builders.get(1); 89 | assertThat(builder.getSignedApkMapping(), instanceOf(SignedApkMappingStrategy.UnsignedApkBuilderDirMapping.class)); 90 | 91 | builder = (SignApksBuilder) builders.get(2); 92 | assertThat(builder.getSignedApkMapping(), instanceOf(SignedApkMappingStrategy.UnsignedApkBuilderDirMapping.class)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/SigningComponents.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | 4 | import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; 5 | 6 | import org.apache.commons.lang.StringUtils; 7 | 8 | import java.io.Serializable; 9 | import java.security.GeneralSecurityException; 10 | import java.security.KeyStore; 11 | import java.security.PrivateKey; 12 | import java.security.UnrecoverableKeyException; 13 | import java.security.cert.Certificate; 14 | import java.util.Enumeration; 15 | 16 | 17 | public class SigningComponents implements Serializable { 18 | 19 | private static final long serialVersionUID = 1L; 20 | 21 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="DCN_NULLPOINTER_EXCEPTION", 22 | justification="getEntry can generate this exeception") 23 | public static SigningComponents fromCredentials(StandardCertificateCredentials creds, String keyAlias) throws GeneralSecurityException { 24 | KeyStore keyStore = creds.getKeyStore(); 25 | if (StringUtils.isEmpty(keyAlias)) { 26 | keyAlias = null; 27 | Enumeration aliases = keyStore.aliases(); 28 | if (aliases != null) { 29 | while (aliases.hasMoreElements()) { 30 | String entryAlias = aliases.nextElement(); 31 | if (keyStore.isKeyEntry(entryAlias)) { 32 | if (keyAlias != null) { 33 | throw new UnrecoverableKeyException("no key alias was given and there is more than one entry in key store"); 34 | } 35 | keyAlias = entryAlias; 36 | } 37 | } 38 | } 39 | } 40 | if (keyAlias == null) { 41 | throw new UnrecoverableKeyException("no key alias was given and no key entries were found in key store"); 42 | } 43 | 44 | String password = creds.getPassword().getPlainText(); 45 | char[] passwordChars = new char[0]; 46 | if (password != null) { 47 | passwordChars = password.toCharArray(); 48 | } 49 | KeyStore.PasswordProtection protection = new KeyStore.PasswordProtection(passwordChars); 50 | KeyStore.PrivateKeyEntry entry; 51 | try { 52 | entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(keyAlias, protection); 53 | } 54 | catch(NullPointerException e) { 55 | // empty passwords could be pessimistically handled, but this way if Credentials Plugin 56 | // changes to load key stores (CertificateCredentialsImpl) with empty password instead 57 | // of null, this should still work 58 | if (StringUtils.isEmpty(password)) { 59 | throw new NullKeyStorePasswordException( 60 | "the password for key store credential " + creds.getId() + " is null - use the Credentials Plugin to configure a non-empty password", e); 61 | } 62 | throw e; 63 | } 64 | if (entry == null) { 65 | throw new GeneralSecurityException("key store credential " + creds.getId() + " has no entry named " + keyAlias); 66 | } 67 | PrivateKey key = entry.getPrivateKey(); 68 | Certificate[] certChain = entry.getCertificateChain(); 69 | 70 | return new SigningComponents(key, certChain, keyAlias, keyAlias); 71 | } 72 | 73 | public final PrivateKey key; 74 | public final Certificate[] certChain; 75 | public final String alias; 76 | public final String v1SigName; 77 | 78 | private SigningComponents(PrivateKey key, Certificate[] certChain, String alias, String v1SigName) { 79 | this.key = key; 80 | this.certChain = certChain; 81 | this.alias = alias; 82 | this.v1SigName = v1SigName; 83 | } 84 | 85 | /** 86 | * Using either a null password or empty password does not work because 87 | * the Credentials Plugin's CertificateCredentialsImpl uses hudson.Util.fixeEmpty() 88 | * on the password, which turns empty strings to null. The plugin then calls 89 | * KeyStore.load() with a null password which results in a NullPointerException 90 | * when calling KeyStore.getEntry(alias). See also ReadingKeyStoresTest.java. 91 | */ 92 | static class NullKeyStorePasswordException extends GeneralSecurityException { 93 | NullKeyStorePasswordException(String message, Throwable cause) { 94 | super(message, cause); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 5.17 9 | 10 | 11 | 12 | android-signing 13 | ${changelist} 14 | hpi 15 | Android Signing Plugin 16 | A Jenkins build step for signing Android APKs with Jenkins-managed credentials 17 | https://github.com/jenkinsci/android-signing-plugin/blob/master/README.md 18 | 19 | 20 | 21 | Apache License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | 24 | 25 | 26 | 27 | 999999-SNAPSHOT 28 | 29 | 2.492 30 | 31 | ${jenkins.baseline}.3 32 | 33 | 34 | 35 | scm:git:https://github.com/${gitHubRepo}.git 36 | scm:git:git@github.com:${gitHubRepo}.git 37 | https://github.com/${gitHubRepo} 38 | ${scmTag} 39 | 40 | 41 | 42 | 43 | 44 | io.jenkins.tools.bom 45 | bom-${jenkins.baseline}.x 46 | 4890.vfca_82c6741a_d 47 | import 48 | pom 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | repo.jenkins-ci.org 57 | https://repo.jenkins-ci.org/public/ 58 | 59 | 60 | 61 | 62 | maven.google.com 63 | https://maven.google.com 64 | 65 | 66 | google 67 | Google Maven Repository 68 | https://dl.google.com/dl/android/maven2/ 69 | 70 | 71 | 72 | 73 | 74 | repo.jenkins-ci.org 75 | https://repo.jenkins-ci.org/public/ 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.jenkins-ci.plugins 83 | credentials 84 | 85 | 86 | org.jenkins-ci 87 | symbol-annotation 88 | 89 | 90 | com.android.tools.build 91 | apksig 92 | 8.10.1 93 | 94 | 95 | org.jenkins-ci.plugins.workflow 96 | workflow-cps 97 | true 98 | 99 | 100 | org.jenkins-ci.plugins 101 | job-dsl 102 | true 103 | 104 | 105 | 106 | 107 | org.mockito 108 | mockito-core 109 | test 110 | 111 | 112 | org.jenkins-ci.plugins.workflow 113 | workflow-basic-steps 114 | test 115 | 116 | 117 | org.jenkins-ci.plugins.workflow 118 | workflow-job 119 | test 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.jenkins-ci.tools 128 | maven-hpi-plugin 129 | 130 | 2.1.0 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/test/groovy/org/jenkinsci/plugins/androidsigning/MultiEntryToSingleEntryBuilderMigrationTest.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning 2 | 3 | import hudson.model.FreeStyleProject 4 | import hudson.model.Items 5 | import hudson.model.listeners.ItemListener 6 | import hudson.tasks.Builder 7 | import hudson.tasks.Shell 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.jvnet.hudson.test.JenkinsRule 11 | import org.jvnet.hudson.test.WithoutJenkins 12 | import org.mockito.Mockito 13 | 14 | import static org.hamcrest.CoreMatchers.* 15 | import static org.junit.Assert.assertThat 16 | 17 | class MultiEntryToSingleEntryBuilderMigrationTest { 18 | 19 | @Rule 20 | public JenkinsRule testJenkins = new JenkinsRule(); 21 | 22 | @Test 23 | @WithoutJenkins 24 | void builderIsNotMigratedIfItHasEntries() throws Exception { 25 | InputStream oldConfigIn = this.class.getResourceAsStream("compatibility/config-2.0.8.xml") 26 | FreeStyleProject job = Items.XSTREAM.fromXML(oldConfigIn) 27 | 28 | assertThat(job.buildersList.size(), equalTo(2)) 29 | 30 | SignApksBuilder builder = job.buildersList[0] 31 | 32 | assertThat(builder.entries.size(), equalTo(3)) 33 | assertThat(builder.isMigrated(), is(false)) 34 | 35 | builder = job.buildersList[1] 36 | 37 | assertThat(builder.entries.size(), equalTo(2)) 38 | assertThat(builder.isMigrated(), is(false)) 39 | 40 | builder.entries.clear() 41 | 42 | assertThat(builder.isMigrated(), is(true)) 43 | } 44 | 45 | @Test 46 | @WithoutJenkins 47 | void builderIsMigratedWhenEntriesIsNull() throws Exception { 48 | SignApksBuilder builder = new SignApksBuilder() 49 | 50 | assertThat(builder.entries, nullValue()) 51 | assertThat(builder.isMigrated(), is(true)) 52 | } 53 | 54 | @Test 55 | void migratesOldData() throws Exception { 56 | MultiEntryToSingleEntryBuilderMigration migration = ItemListener.all().find { it instanceof MultiEntryToSingleEntryBuilderMigration } as MultiEntryToSingleEntryBuilderMigration 57 | InputStream configIn = this.class.getResourceAsStream("compatibility/config-2.0.8.xml") 58 | FreeStyleProject job = Mockito.spy(Items.XSTREAM.fromXML(configIn)) as FreeStyleProject 59 | testJenkins.jenkins.add(job, this.class.simpleName) 60 | job.onLoad(testJenkins.jenkins, this.class.simpleName) 61 | 62 | assertThat(job.buildersList.size(), equalTo(2)) 63 | 64 | int oldEntryCount = job.buildersList.sum { SignApksBuilder builder -> builder.entries.size() } as int 65 | 66 | migration.onLoaded() 67 | 68 | Mockito.verify(job).save() 69 | assertThat(job.buildersList.size(), equalTo(oldEntryCount)) 70 | } 71 | 72 | @Test 73 | void doesNotMigrateAlreadyMigratedData() throws Exception { 74 | MultiEntryToSingleEntryBuilderMigration migration = ItemListener.all().find { it instanceof MultiEntryToSingleEntryBuilderMigration } as MultiEntryToSingleEntryBuilderMigration 75 | InputStream configIn = this.class.getResourceAsStream("compatibility/config-2.1.0.xml") 76 | FreeStyleProject job = Mockito.spy(Items.XSTREAM.fromXML(configIn)) as FreeStyleProject 77 | testJenkins.jenkins.add(job, this.class.simpleName) 78 | job.onLoad(testJenkins.jenkins, this.class.simpleName) 79 | 80 | List loadedBuilders = new ArrayList<>(job.builders) 81 | loadedBuilders.each { assertThat(((SignApksBuilder) it).migrated, is(true))} 82 | 83 | migration.onLoaded() 84 | 85 | Mockito.verify(job, Mockito.never()).save() 86 | assertThat(job.builders.size(), equalTo(loadedBuilders.size())) 87 | job.builders.eachWithIndex { Builder entry, int i -> 88 | assertThat(entry, sameInstance(loadedBuilders[i])) 89 | } 90 | } 91 | 92 | @Test 93 | void leavesOtherBuildStepsInPlace() throws Exception { 94 | MultiEntryToSingleEntryBuilderMigration migration = ItemListener.all().find { it instanceof MultiEntryToSingleEntryBuilderMigration } as MultiEntryToSingleEntryBuilderMigration 95 | InputStream configIn = this.class.getResourceAsStream("compatibility/config-2.0.8.xml") 96 | FreeStyleProject job = Items.XSTREAM.fromXML(configIn) as FreeStyleProject 97 | testJenkins.jenkins.add(job, this.class.simpleName) 98 | job.onLoad(testJenkins.jenkins, this.class.simpleName) 99 | 100 | assertThat(job.buildersList.size(), equalTo(2)) 101 | 102 | int oldEntryCount = job.buildersList.sum { SignApksBuilder builder -> builder.entries.size() } as int 103 | 104 | List buildersMod = new ArrayList<>(job.buildersList) 105 | buildersMod.add(1, new Shell("echo \"${this.class}\"")) 106 | job.buildersList.replaceBy(buildersMod) 107 | 108 | migration.onLoaded() 109 | 110 | assertThat(job.buildersList.size(), equalTo(oldEntryCount + 1)) 111 | assertThat(job.buildersList[0], instanceOf(SignApksBuilder)) 112 | assertThat(job.buildersList[1], instanceOf(SignApksBuilder)) 113 | assertThat(job.buildersList[2], instanceOf(SignApksBuilder)) 114 | assertThat(job.buildersList[3], instanceOf(Shell)) 115 | assertThat(job.buildersList[4], instanceOf(SignApksBuilder)) 116 | assertThat(job.buildersList[5], instanceOf(SignApksBuilder)) 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/SignApksStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import com.google.inject.Inject; 4 | 5 | import org.apache.commons.lang.StringUtils; 6 | import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; 7 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; 8 | import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; 9 | import org.jenkinsci.plugins.workflow.steps.StepContextParameter; 10 | import org.jenkinsci.plugins.workflow.structs.DescribableHelper; 11 | import org.kohsuke.stapler.DataBoundConstructor; 12 | import org.kohsuke.stapler.DataBoundSetter; 13 | 14 | import edu.umd.cs.findbugs.annotations.CheckForNull; 15 | import edu.umd.cs.findbugs.annotations.NonNull; 16 | 17 | import hudson.EnvVars; 18 | import hudson.Extension; 19 | import hudson.FilePath; 20 | import hudson.Launcher; 21 | import hudson.model.Run; 22 | import hudson.model.TaskListener; 23 | 24 | 25 | public class SignApksStep extends AbstractStepImpl { 26 | 27 | @CheckForNull 28 | private String keyStoreId; 29 | @CheckForNull 30 | private String keyAlias; 31 | @CheckForNull 32 | private String apksToSign; 33 | private SignedApkMappingStrategy signedApkMapping; 34 | private String androidHome; 35 | private String zipalignPath; 36 | private boolean skipZipalign = false; 37 | private boolean archiveSignedApks = true; 38 | private boolean archiveUnsignedApks = false; 39 | 40 | @DataBoundConstructor 41 | public SignApksStep() { 42 | } 43 | 44 | @DataBoundSetter 45 | public void setKeyStoreId(String x) { 46 | keyStoreId = x; 47 | } 48 | 49 | @DataBoundSetter 50 | public void setKeyAlias(String x) { 51 | keyAlias = x; 52 | } 53 | 54 | @DataBoundSetter 55 | public void setApksToSign(String x) { 56 | apksToSign = x; 57 | } 58 | 59 | @DataBoundSetter 60 | public void setSignedApkMapping(SignedApkMappingStrategy x) { 61 | signedApkMapping = x; 62 | } 63 | 64 | @DataBoundSetter 65 | public void setSkipZipalign(boolean x) { 66 | skipZipalign = x; 67 | } 68 | 69 | @DataBoundSetter 70 | public void setArchiveSignedApks(boolean x) { 71 | archiveSignedApks = x; 72 | } 73 | 74 | @DataBoundSetter 75 | public void setArchiveUnsignedApks(boolean x) { 76 | archiveUnsignedApks = x; 77 | } 78 | 79 | @DataBoundSetter 80 | public void setAndroidHome(String x) { 81 | androidHome = x; 82 | } 83 | 84 | @DataBoundSetter 85 | public void setZipalignPath(String x) { 86 | zipalignPath = x; 87 | } 88 | 89 | public String getKeyStoreId() { 90 | return keyStoreId; 91 | } 92 | 93 | public String getKeyAlias() { 94 | return keyAlias; 95 | } 96 | 97 | public String getApksToSign() { 98 | return apksToSign; 99 | } 100 | 101 | public SignedApkMappingStrategy getSignedApkMapping() { 102 | return signedApkMapping; 103 | } 104 | 105 | public boolean getSkipZipalign() { 106 | return skipZipalign; 107 | } 108 | 109 | public boolean getArchiveSignedApks() { 110 | return archiveSignedApks; 111 | } 112 | 113 | public boolean getArchiveUnsignedApks() { 114 | return archiveUnsignedApks; 115 | } 116 | 117 | public String getAndroidHome() { 118 | return androidHome; 119 | } 120 | 121 | public String getZipalignPath() { 122 | return zipalignPath; 123 | } 124 | 125 | 126 | private static class SignApksStepExecution extends AbstractSynchronousNonBlockingStepExecution { 127 | 128 | private static final long serialVersionUID = 1L; 129 | 130 | @Inject 131 | private transient SignApksStep step; 132 | 133 | @StepContextParameter 134 | @SuppressWarnings("unused") 135 | private transient Run build; 136 | 137 | @StepContextParameter 138 | @SuppressWarnings("unused") 139 | private transient FilePath workspace; 140 | 141 | @StepContextParameter 142 | @SuppressWarnings("unused") 143 | private transient Launcher launcher; 144 | 145 | @StepContextParameter 146 | @SuppressWarnings("unused") 147 | private transient TaskListener listener; 148 | 149 | @StepContextParameter 150 | private transient EnvVars env; 151 | 152 | @Override 153 | protected Void run() throws Exception { 154 | String androidHome = step.getAndroidHome(); 155 | String zipalignPath = step.getZipalignPath(); 156 | if (StringUtils.isEmpty(androidHome) && StringUtils.isEmpty(zipalignPath)) { 157 | if (StringUtils.isEmpty(androidHome)) { 158 | androidHome = env.get(ZipalignTool.ENV_ANDROID_HOME); 159 | } 160 | if (StringUtils.isEmpty(zipalignPath)) { 161 | zipalignPath = env.get(ZipalignTool.ENV_ZIPALIGN_PATH); 162 | } 163 | } 164 | SignApksBuilder builder = new SignApksBuilder(); 165 | builder.setKeyStoreId(step.getKeyStoreId()); 166 | builder.setKeyAlias(step.getKeyAlias()); 167 | builder.setApksToSign(step.getApksToSign()); 168 | builder.setSignedApkMapping(step.getSignedApkMapping()); 169 | builder.setSkipZipalign(step.getSkipZipalign()); 170 | builder.setArchiveSignedApks(step.getArchiveSignedApks()); 171 | builder.setArchiveUnsignedApks(step.getArchiveUnsignedApks()); 172 | builder.setAndroidHome(androidHome); 173 | builder.setZipalignPath(zipalignPath); 174 | builder.perform(build, workspace, launcher, listener); 175 | return null; 176 | } 177 | } 178 | 179 | @Extension(optional = true) 180 | public static class DescriptorImpl extends AbstractStepDescriptorImpl { 181 | public DescriptorImpl() { 182 | super(SignApksStepExecution.class); 183 | } 184 | 185 | @Override 186 | public String getFunctionName() { 187 | return "signAndroidApks"; 188 | } 189 | 190 | @NonNull 191 | @Override 192 | public String getDisplayName() { 193 | return Messages.builderDisplayName(); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Jenkins Android Signing Plugin 2 | # Version History 3 | 4 | ## 2.2.5 - 23 July 2017 5 | * Fix [JENKINS-45714](https://issues.jenkins-ci.org/browse/JENKINS-45714) 6 | * Added missing concerns for finding the `zipalign.exe` command and Android SDK on Windows. 7 | * Properly override the build environment with the effective launched process environment so 8 | the plugin can find `zipalign` from a [custom tool](https://plugins.jenkins.io/custom-tools-plugin) 9 | exported `PATH` element. 10 | 11 | ## 2.2.4 - 14 June 2017 12 | Enhancements 13 | * Attempt to retrieve environment variables from a dummy process to find the `zipalign` utility. 14 | As a result, you can use the [Custom Tools](https://plugins.jenkins.io/custom-tools-plugin) Plugin 15 | to install the Android SDK and export `ANDROID_HOME` environment variable. This is necessary 16 | because the Custom Tool plugin only adds environment variables to 17 | [launched](http://javadoc.jenkins-ci.org/hudson/Launcher.DecoratedLauncher.html) processes for a build, 18 | e.g., an _Execute shell_ build step. 19 | * Search the `PATH` variable for the `zipalign` utility and potential Android SDK installations. This 20 | suggestion comes from [JENKINS-41787](https://issues.jenkins-ci.org/browse/JENKINS-41787). 21 | 22 | ## 2.2.3 - 30 May 2017 23 | Minor bug fix/enhancement ([JENKINS-44428](https://issues.jenkins-ci.org/browse/JENKINS-44428)) 24 | * Use the credentials ID in the _Key Store_ drop-down text if the credentials description is blank. 25 | * Added more meaningful error log messages when things go wrong reading the key store. 26 | * You can now leave the _Key Alias_ field blank/null when your key store only has one key entry. 27 | 28 | ## 2.2.2 - 19 May 2017 29 | Minor bug fix ([JENKINS-44299](https://issues.jenkins-ci.org/browse/JENKINS-44299)) 30 | * The _Sign Android APKs_ build step form validation now catches the `InterruptedException` that occurs in the above issue, 31 | as well as validates all globs in a comma-separated multi-value of the _APKs to Sign_ form field. 32 | 33 | ## 2.2.1 - 10 April 2016 34 | * Omit the `-signed` component of the output signed APKs for the _Output to unsigned APK sibling_/`UnsignedApkSiblingMapping` 35 | option when the unsigned APK includes the `-unsigned` component. This is to align with the intention of the change in the 36 | previous release to write signed APKs the same way the Android Gradle plugin does. Apologies for the excessive changes. 37 | 38 | ## 2.2.0 - 3 April 2016 39 | * Skip zipalign option - This is primarily to support signing debug APKs, for which the zipalign command fails. 40 | Choose this option in the advanced section of the config UI 41 | * Signed APK output option - Signed APKs are now written to the directory of the input unsigned APK by default. Change this option in the advanced section of 42 | the config UI. 43 | * Previously saved _Sign APKs_ steps of Freestyle jobs will output signed APKs as before to the 44 | `SignApksBuilder-out` directory. 45 | * Pipeline scripts with `signAndroidApks` steps will write signed APKs in the new default manner. 46 | You can specify the old behavior like the this: 47 | ```groovy 48 | signAndroidApks ( 49 | // ... 50 | signedApkMapping: [$class: 'UnsignedApkBuilderDirMapping'] 51 | ) 52 | ``` 53 | * _Sign APKs_ steps previously or newly generated from a _Job DSL_ plugin script will write signed APKs in the new default manner. You can specify the old 54 | behavior like this: 55 | ```groovy 56 | job('myAndroidApp.seed') { 57 | steps { 58 | signAndroidApks '**/*-unsigned.apk', { 59 | signedApkMapping { unsignedApkNameDir() } 60 | } 61 | } 62 | } 63 | ``` 64 | * Updated Pipeline/Workflow dependency to 2.0 65 | * Updated Android apksig dependency to 2.3.0 release 66 | 67 | ## 2.1.0 - 1 Mar 2017 68 | This release includes some significant changes: 69 | * Config format change that is not backwards compatible 70 | * The Sign APKs build step no longer accepts multiple APK signing entries. You must instead use multiple Sign APKs build steps. However, in most cases, if you are actually signing multiple APKs, the comma-separated globs in the APKs to Sign field should cover your needs with just one build step. 71 | * When you install the new version of the plugin and restart Jenkins, the plugin will automatically upgrade any of your jobs that include Sign APKs build steps. 72 | * Be aware that you **cannot downgrade** the plugin after upgrading and expect your Sign APKs build step configurations to remain intact. 73 | * Jenkins [Pipeline](https://jenkins.io/doc/book/pipeline/) support - see the [README](README.md) 74 | * [Job DSL](https://github.com/jenkinsci/job-dsl-plugin/wiki) support - see the [README](README.md) 75 | * As part of the the Pipeline support, the build step configuration form now offers parameters to override the `ANDROID_HOME` location or the Zipalign Path (`ANDROID_ZIPALIGN` environment variable) directly. Access these fields by clicking the _Advanced_ button at the top of the _Sign APKs_ form group. _Zipalign Path_ takes precedence over `ANDROID_HOME`. If you don't supply either of these configuration parameters in the form, the plugin will still attempt to find them from the Jenkins system environment variables. 76 | * Build step form fields help on the configuration page 77 | * The _Archive Signed APKs_ option now defaults to checked/true 78 | * Improved naming of output APKs 79 | * The plugin will produce signed APKs relative to the job workspace at `SignApksBuilder-out/myApp-unsigned.apk/myApp-signed.apk` in case you have downstream build steps that will manipulate the signed APK. 80 | * The plugin no longer assumes the input, unsigned APKs have a `-unsigned` component in the file name. If `-unsigned` is present, the plugin will replace it with `-signed`. Otherwise, the plugin will simply insert `-signed` before the `.apk` suffix. 81 | 82 | ## 2.0.8 - 7 Feb 2017 83 | Minor bug fixes 84 | * Try using `zipalign.exe` if `zipalign` does not exist in `ANDROID_HOME` for Windows case 85 | * Expand the zipalign path with EnvVars 86 | 87 | ## 2.0.7 - 18 Jan 2017 88 | This is the first release since the [original plugin](https://github.com/bignerdranch/jenkins-android-signing) became unmaintained two years ago. 89 | * Updated to use [APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) 90 | * Choices to archive the unsigned and signed APKs 91 | * Auto-discover the [zipalign command](https://developer.android.com/studio/command-line/zipalign.html) from `ANDROID_HOME`, or override with `ANDROID_ZIPALIGN` 92 | * [Folder-scope-aware](https://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Folders+Plugin) credentials lookup -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/ReadingKeyStoresTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | 4 | import org.junit.BeforeClass; 5 | import org.junit.Test; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | import java.security.Key; 11 | import java.security.KeyFactory; 12 | import java.security.KeyStore; 13 | import java.security.PrivateKey; 14 | import java.security.UnrecoverableEntryException; 15 | import java.security.UnrecoverableKeyException; 16 | import java.security.cert.Certificate; 17 | import java.security.cert.CertificateFactory; 18 | import java.security.spec.PKCS8EncodedKeySpec; 19 | import java.util.Base64; 20 | 21 | import static org.hamcrest.CoreMatchers.equalTo; 22 | import static org.hamcrest.CoreMatchers.not; 23 | import static org.hamcrest.CoreMatchers.nullValue; 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.junit.Assert.assertTrue; 26 | 27 | public class ReadingKeyStoresTest { 28 | 29 | static PrivateKey basePemKey; 30 | static Certificate basePemCert; 31 | 32 | @BeforeClass 33 | public static void parseBaseKeys() throws Exception { 34 | BufferedReader in = new BufferedReader(new InputStreamReader( 35 | ReadingKeyStoresTest.class.getResourceAsStream("/SignApksBuilderTest-key-exposed.pkcs8.pem"))); 36 | StringBuilder bareBase64 = new StringBuilder(); 37 | String line; 38 | while ((line = in.readLine()) != null) { 39 | if (!line.contains("PRIVATE KEY")) { 40 | bareBase64.append(line.replaceAll("\\s", "")); 41 | } 42 | } 43 | byte[] keyBytes = Base64.getDecoder().decode(bareBase64.toString()); 44 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); 45 | KeyFactory rsaKeys = KeyFactory.getInstance("RSA"); 46 | basePemKey = rsaKeys.generatePrivate(keySpec); 47 | 48 | CertificateFactory certs = CertificateFactory.getInstance("X.509"); 49 | basePemCert = certs.generateCertificate(ReadingKeyStoresTest.class.getResourceAsStream("/SignApksBuilderTest.pem")); 50 | } 51 | 52 | @Test 53 | public void loadKeyStoreWithPassword() throws Exception { 54 | InputStream keyStoreIn = getClass().getResourceAsStream("/SignApksBuilderTest.p12"); 55 | KeyStore store = KeyStore.getInstance("PKCS12"); 56 | store.load(keyStoreIn, "SignApksBuilderTest".toCharArray()); 57 | 58 | assertTrue(store.containsAlias("SignApksBuilderTest")); 59 | 60 | KeyStore.PasswordProtection protection = new KeyStore.PasswordProtection("SignApksBuilderTest".toCharArray()); 61 | KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) store.getEntry("SignApksBuilderTest", protection); 62 | PrivateKey key = (PrivateKey) store.getKey("SignApksBuilderTest", protection.getPassword()); 63 | Certificate[] chain = store.getCertificateChain("SignApksBuilderTest"); 64 | 65 | assertThat(entry.getPrivateKey(), equalTo(basePemKey)); 66 | assertThat(key, equalTo(basePemKey)); 67 | 68 | assertThat(entry.getCertificateChain(), not(nullValue())); 69 | assertThat(entry.getCertificateChain().length, equalTo(1)); 70 | assertThat(entry.getCertificateChain()[0], equalTo(basePemCert)); 71 | 72 | assertThat(chain.length, equalTo(1)); 73 | assertThat(chain[0], equalTo(basePemCert)); 74 | } 75 | 76 | @Test 77 | public void loadAKeyStoreWithBlankPassword() throws Exception { 78 | InputStream keyStoreIn = getClass().getResourceAsStream("/SignApksBuilderTest-exposed.p12"); 79 | KeyStore store = KeyStore.getInstance("PKCS12"); 80 | store.load(keyStoreIn, new char[0]); 81 | 82 | assertTrue(store.containsAlias("SignApksBuilderTest-exposed")); 83 | 84 | KeyStore.PasswordProtection prot = new KeyStore.PasswordProtection(new char[0]); 85 | KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) store.getEntry("SignApksBuilderTest-exposed", prot); 86 | Key key = store.getKey("SignApksBuilderTest-exposed", prot.getPassword()); 87 | Certificate[] chain = store.getCertificateChain("SignApksBuilderTest-exposed"); 88 | 89 | assertThat(entry.getPrivateKey(), equalTo(basePemKey)); 90 | assertThat(key, equalTo(basePemKey)); 91 | 92 | assertThat(entry.getCertificateChain(), not(nullValue())); 93 | assertThat(entry.getCertificateChain().length, equalTo(1)); 94 | assertThat(entry.getCertificateChain()[0], equalTo(basePemCert)); 95 | 96 | assertThat(chain.length, equalTo(1)); 97 | assertThat(chain[0], equalTo(basePemCert)); 98 | } 99 | 100 | @Test 101 | public void doesNotWorkWithoutProvidingAKeyPasswordMatchingStorePassword() throws Exception { 102 | InputStream keyStoreIn = getClass().getResourceAsStream("/SignApksBuilderTest-noKeyPass.p12"); 103 | KeyStore store = KeyStore.getInstance("PKCS12"); 104 | store.load(keyStoreIn, "SignApksBuilderTest-noKeyPass".toCharArray()); 105 | 106 | assertTrue(store.containsAlias("SignApksBuilderTest-noKeyPass")); 107 | 108 | KeyStore.PasswordProtection prot = new KeyStore.PasswordProtection(new char[0]); 109 | KeyStore.PrivateKeyEntry entry = null; 110 | try { 111 | entry = (KeyStore.PrivateKeyEntry) store.getEntry("SignApksBuilderTest-noKeyPass", prot); 112 | } 113 | catch (UnrecoverableEntryException e) { 114 | } 115 | Key key = null; 116 | try { 117 | key = store.getKey("SignApksBuilderTest-noKeyPass", prot.getPassword()); 118 | } 119 | catch (UnrecoverableKeyException e) { 120 | } 121 | 122 | assertThat(entry, nullValue()); 123 | assertThat(key, nullValue()); 124 | 125 | prot = new KeyStore.PasswordProtection("SignApksBuilderTest-noKeyPass".toCharArray()); 126 | entry = (KeyStore.PrivateKeyEntry) store.getEntry("SignApksBuilderTest-noKeyPass", prot); 127 | key = store.getKey("SignApksBuilderTest-noKeyPass", prot.getPassword()); 128 | 129 | assertThat(entry.getPrivateKey(), equalTo(basePemKey)); 130 | assertThat(key, equalTo(basePemKey)); 131 | } 132 | 133 | @Test(expected = NullPointerException.class) 134 | public void loadingPasswordlessKeyStoreWithNullPasswordInsteadOfEmptyDoesNotWork() throws Exception { 135 | InputStream keyStoreIn = getClass().getResourceAsStream("/SignApksBuilderTest-exposed.p12"); 136 | KeyStore store = KeyStore.getInstance("PKCS12"); 137 | store.load(keyStoreIn, null); 138 | 139 | assertTrue(store.containsAlias("SignApksBuilderTest-exposed")); 140 | 141 | KeyStore.PasswordProtection prot = new KeyStore.PasswordProtection(new char[0]); 142 | KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) store.getEntry("SignApksBuilderTest-exposed", prot); 143 | Key key = store.getKey("SignApksBuilderTest-exposed", prot.getPassword()); 144 | Certificate[] chain = store.getCertificateChain("SignApksBuilderTest-exposed"); 145 | 146 | assertThat(entry.getPrivateKey(), equalTo(basePemKey)); 147 | assertThat(entry.getCertificateChain(), not(nullValue())); 148 | assertThat(entry.getCertificateChain().length, equalTo(1)); 149 | assertThat(entry.getCertificateChain()[0], equalTo(basePemCert)); 150 | 151 | assertThat(key, equalTo(basePemKey)); 152 | 153 | assertThat(chain.length, equalTo(1)); 154 | assertThat(chain[0], equalTo(basePemCert)); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/ZipalignTool.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | import java.util.List; 8 | import java.util.SortedMap; 9 | import java.util.TreeMap; 10 | 11 | import edu.umd.cs.findbugs.annotations.NonNull; 12 | import edu.umd.cs.findbugs.annotations.Nullable; 13 | 14 | import hudson.AbortException; 15 | import hudson.EnvVars; 16 | import hudson.FilePath; 17 | import hudson.util.ArgumentListBuilder; 18 | import hudson.util.VersionNumber; 19 | 20 | 21 | class ZipalignTool { 22 | 23 | static final String ENV_ANDROID_HOME = "ANDROID_HOME"; 24 | static final String ENV_ZIPALIGN_PATH = "ANDROID_ZIPALIGN"; 25 | static final String ENV_PATH = "PATH"; 26 | 27 | private static FilePath findFromEnv(EnvVars env, FilePath workspace, PrintStream logger) throws AbortException { 28 | 29 | String zipalignPath = env.get(ENV_ZIPALIGN_PATH); 30 | if (!StringUtils.isEmpty(zipalignPath)) { 31 | zipalignPath = env.expand(zipalignPath); 32 | logger.printf("[SignApksBuilder] found zipalign path in env %s=%s%n", ENV_ZIPALIGN_PATH, zipalignPath); 33 | FilePath zipalign = new FilePath(workspace.getChannel(), zipalignPath); 34 | return zipalignOrZipalignExe(zipalign, logger); 35 | } 36 | 37 | String androidHome = env.get(ENV_ANDROID_HOME); 38 | if (!StringUtils.isEmpty(androidHome)) { 39 | androidHome = env.expand(androidHome); 40 | logger.printf("[SignApksBuilder] searching environment variable %s=%s for zipalign...%n", ENV_ANDROID_HOME, androidHome); 41 | return findInAndroidHome(androidHome, workspace, logger); 42 | } 43 | 44 | String envPath = env.get(ENV_PATH); 45 | if (!StringUtils.isEmpty(envPath)) { 46 | envPath = env.expand(envPath); 47 | logger.printf("[SignApksBuilder] searching environment %s=%s for zipalign...%n", ENV_PATH, envPath); 48 | return findInPathEnvVar(envPath, workspace, logger); 49 | } 50 | 51 | throw new AbortException("failed to find zipalign: no environment variable " + ENV_ZIPALIGN_PATH + " or " + ENV_ANDROID_HOME + " or " + ENV_PATH); 52 | } 53 | 54 | private static FilePath findInAndroidHome(String androidHome, FilePath workspace, PrintStream logger) throws AbortException { 55 | 56 | FilePath buildTools = workspace.child(androidHome).child("build-tools"); 57 | List versionDirs; 58 | try { 59 | versionDirs = buildTools.listDirectories(); 60 | } 61 | catch (Exception e) { 62 | e.printStackTrace(logger); 63 | throw new AbortException(String.format( 64 | "failed to find zipalign: error listing build-tools versions in %s: %s", 65 | buildTools.getRemote(), e.getLocalizedMessage())); 66 | } 67 | 68 | if (versionDirs == null || versionDirs.isEmpty()) { 69 | throw new AbortException("failed to find zipalign: no build-tools directory in Android home path " + androidHome); 70 | } 71 | 72 | SortedMap versions = new TreeMap<>(); 73 | for (FilePath versionDir : versionDirs) { 74 | String versionName = versionDir.getName(); 75 | VersionNumber version = new VersionNumber(versionName); 76 | versions.put(version, versionDir); 77 | } 78 | 79 | if (versions.isEmpty()) { 80 | throw new AbortException( 81 | "failed to find zipalign: no build-tools versions in Android home path " + buildTools); 82 | } 83 | 84 | VersionNumber latest = versions.lastKey(); 85 | buildTools = versions.get(latest); 86 | FilePath zipalign = zipalignOrZipalignExe(buildTools, logger); 87 | if (zipalign != null) { 88 | logger.printf("[SignApksBuilder] found zipalign in Android SDK's latest build tools: %s%n", zipalign.getRemote()); 89 | return zipalign; 90 | } 91 | 92 | throw new AbortException("failed to find zipalign: no zipalign found in latest Android build tools: " + buildTools); 93 | } 94 | 95 | private static FilePath findInPathEnvVar(String envPath, FilePath workspace, PrintStream logger) throws AbortException { 96 | String separator = null; 97 | try { 98 | separator = pathSeparatorForWorkspace(workspace); 99 | } 100 | catch (Exception e) { 101 | logger.println("[SignApksBuilder] error determining path separator:"); 102 | e.printStackTrace(logger); 103 | return null; 104 | } 105 | String[] dirs = envPath.split(separator); 106 | for (String dir : dirs) { 107 | logger.printf("[SignApksBuilder] checking %s dir %s for zipalign...%n", ENV_PATH, dir); 108 | FilePath dirPath = workspace.child(dir); 109 | FilePath zipalign = zipalignOrZipalignExe(dirPath, logger); 110 | if (zipalign != null) { 111 | return zipalign; 112 | } 113 | try { 114 | dirPath = androidHomeAncestorOfPath(dirPath, logger); 115 | } 116 | catch (Exception e) { 117 | logger.println("error searching " + ENV_PATH + " environment variable: " + e.getMessage()); 118 | e.printStackTrace(logger); 119 | } 120 | if (dirPath != null) { 121 | logger.printf("[SignApksBuilder] found potential Android home in %s dir %s%n", ENV_PATH, dir); 122 | try { 123 | return findInAndroidHome(dirPath.getRemote(), workspace, logger); 124 | } 125 | catch (AbortException e) { 126 | logger.printf("error searching Android home found in " + ENV_PATH + ": " + e.getMessage()); 127 | } 128 | } 129 | } 130 | 131 | return null; 132 | } 133 | 134 | private static String pathSeparatorForWorkspace(FilePath workspace) throws IOException, InterruptedException { 135 | return workspace.act(new GetPathSeparator()); 136 | } 137 | 138 | private static FilePath androidHomeAncestorOfPath(FilePath path, PrintStream logger) throws Exception { 139 | if ("bin".equals(path.getName())) { 140 | FilePath sdkmanager = path.child("sdkmanager"); 141 | if (commandOrWinCommandAtPath(sdkmanager, logger) != null) { 142 | path = path.getParent(); 143 | if (path != null && "tools".equals(path.getName())) { 144 | return path.getParent(); 145 | } 146 | } 147 | } 148 | else if ("tools".equals(path.getName())) { 149 | FilePath androidTool = path.child("android"); 150 | if (commandOrWinCommandAtPath(androidTool, logger) != null) { 151 | return path.getParent(); 152 | } 153 | } 154 | else { 155 | FilePath androidTool = path.child("tools").child("android"); 156 | if (commandOrWinCommandAtPath(androidTool, logger) != null) { 157 | return path; 158 | } 159 | } 160 | 161 | return null; 162 | } 163 | 164 | private static FilePath zipalignOrZipalignExe(FilePath zipalignOrDir, PrintStream logger) { 165 | FilePath parent = zipalignOrDir.getParent(); 166 | try { 167 | if (zipalignOrDir.isDirectory()) { 168 | parent = zipalignOrDir; 169 | zipalignOrDir = zipalignOrDir.child("zipalign"); 170 | } 171 | } 172 | catch (Exception e) { 173 | logger.println("[SignApksBuilder] error checking for zipalign at path " + zipalignOrDir); 174 | e.printStackTrace(logger); 175 | } 176 | zipalignOrDir = commandOrWinCommandAtPath(zipalignOrDir, logger); 177 | if (zipalignOrDir != null) { 178 | return zipalignOrDir; 179 | } 180 | 181 | logger.println("[SignApksBuilder] no zipalign or zipalign.exe found in path " + parent); 182 | return null; 183 | } 184 | 185 | private static FilePath commandOrWinCommandAtPath(FilePath path, PrintStream logger) { 186 | try { 187 | if (path.isDirectory()) { 188 | return null; 189 | } 190 | if (path.exists()) { 191 | return path; 192 | } 193 | FilePath parent = path.getParent(); 194 | if (parent == null) { 195 | return null; 196 | } 197 | String name = path.getName(); 198 | String winCommand = name + ".exe"; 199 | path = parent.child(winCommand); 200 | if (path.exists()) { 201 | return path; 202 | } 203 | winCommand = name + ".bat"; 204 | path = parent.child(winCommand); 205 | if (path.exists()) { 206 | return path; 207 | } 208 | } 209 | catch (Exception e) { 210 | logger.println("[SignApksBuilder] error checking path " + path); 211 | e.printStackTrace(logger); 212 | } 213 | 214 | return null; 215 | } 216 | 217 | private final EnvVars buildEnv; 218 | private final FilePath workspace; 219 | private final PrintStream logger; 220 | private final String overrideAndroidHome; 221 | private final String overrideZipalignPath; 222 | private FilePath zipalign; 223 | 224 | ZipalignTool(@NonNull EnvVars buildEnv, @NonNull FilePath workspace, @NonNull PrintStream logger, @Nullable String overrideAndroidHome, @Nullable String overrideZipalignPath) { 225 | this.buildEnv = buildEnv; 226 | this.workspace = workspace; 227 | this.logger = logger; 228 | this.overrideAndroidHome = overrideAndroidHome; 229 | this.overrideZipalignPath = overrideZipalignPath; 230 | } 231 | 232 | ArgumentListBuilder commandFor(String unsignedApk, String outputApk) throws AbortException { 233 | if (zipalign == null) { 234 | if (!StringUtils.isEmpty(overrideZipalignPath)) { 235 | logger.printf("[SignApksBuilder] zipalign path explicitly set to %s%n", overrideZipalignPath); 236 | zipalign = zipalignOrZipalignExe(workspace.child(buildEnv.expand(overrideZipalignPath)), logger); 237 | } 238 | else if (!StringUtils.isEmpty(overrideAndroidHome)) { 239 | logger.printf("[SignApksBuilder] zipalign %s explicitly set to %s%n", ENV_ANDROID_HOME, overrideAndroidHome); 240 | String expandedAndroidHome = buildEnv.expand(overrideAndroidHome); 241 | zipalign = findInAndroidHome(expandedAndroidHome, workspace, this.logger); 242 | } 243 | else { 244 | zipalign = findFromEnv(buildEnv, workspace, logger); 245 | } 246 | 247 | if (zipalign == null) { 248 | throw new AbortException("failed to find zipalign path in parameters or environment"); 249 | } 250 | } 251 | 252 | return new ArgumentListBuilder() 253 | .add(zipalign.getRemote()) 254 | .add("-P") 255 | .add("16") 256 | .add("-f") 257 | .add("4") 258 | .add(unsignedApk) 259 | .add(outputApk); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jenkins Android Signing Plugin 2 | ============ 3 | 4 | ## Summary and Purpose 5 | 6 | The Android Signing plugin provides a simple build step for 7 | [signing Android APK](https://developer.android.com/studio/publish/app-signing.html#signing-manually) 8 | build artifacts. The advantage of this plugin is that you can use Jenkins to 9 | centrally manage, protect, and provide all of your Android release signing 10 | certificates without the need to distribute private keys and passwords to 11 | every developer. This is especially useful in multi-node/cloud environments 12 | so you do not need to copy the signing keystore to every Jenkins node. 13 | Furthermore, using this plugin externalizes your keystore and private key 14 | passwords from your build script, and keeps them encrypted rather than storing 15 | them in a plain-text properties file. This plugin also does not use a shell 16 | command to perform the signing, eliminating the potential that private key 17 | passwords appear on a command-line invocation. 18 | 19 | ## Background 20 | 21 | This version is a fork from Big Nerd Ranch's now deprecated 22 | [original repository](https://github.com/bignerdranch/jenkins-android-signing) 23 | which is the basis of a nice blog post, 24 | [Continuous Delivery for Android](https://www.bignerdranch.com/blog/continuous-delivery-for-android/). 25 | Thanks to Big Nerd Ranch for the original work. 26 | 27 | This plugin depends on the 28 | [Jenkins Credentials Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Credentials+Plugin) 29 | for retrieving private key credentials for signing APKs. Thanks to 30 | [CloudBees](https://www.cloudbees.com/) and 31 | [Stephen Connolly](https://github.com/stephenc) for the Credentials Plugin. 32 | 33 | This plugin also depends on Android's [`apksig`](https://android.googlesource.com/platform/tools/apksig/) 34 | library to sign APKs programmatically. `apksig` backs the [`apksigner`](https://developer.android.com/studio/command-line/apksigner.html) 35 | utility in the Android SDK Developer Tools package. Using `apksig` ensures the signed APKs 36 | this plugin produces comply with the newer 37 | [APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) and [APK Signature Scheme v3](https://source.android.com/security/apksigning/v3). 38 | Thanks to Google/Android for making that library available as a 39 | [Maven dependency](https://bintray.com/android/android-tools/com.android.tools.build.apksig). 40 | 41 | ## Building 42 | 43 | Run `mvn package` to build a deployable HPI bundle for Jenkins. 44 | 45 | ## Installation 46 | 47 | First, make sure your Jenkins instance has the Credentials Plugin (linked above). 48 | Check the [POM](pom.xml) for version requirements. Copy the `target/android-signing.hpi` 49 | plugin bundle to `$JENKINS_HOME/plugins/` directory, and restart Jenkins. 50 | 51 | As of this writing, this plugin is not yet hosted in the Jenkins Update Centre, so you 52 | cannot install using the Jenkins UI. 53 | 54 | ## Usage 55 | 56 | Before adding a _Sign Android APKs_ build step to a job, you must configure a certificate 57 | credential using the Credentials Plugin's UI. As of this writing, this plugin 58 | requires a password-protected PKCS12 keystore containing a private key entry 59 | protected by the same password. Because of how the Credentials Plugin loads 60 | key stores, you must protect your key store with a non-empty password. You 61 | were doing that anyway, right? 62 | 63 | This plugin requires access to the Android SDK's 64 | [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html) 65 | command. This implies that whatever Jenkins node is performing your build has 66 | access to an installed Android SDK, which is likely the case if you built your 67 | APK in a Jenkins job as well. 68 | 69 | Once the prerequisites are setup, you can now add the _Sign Android APKs_ build step to 70 | a job. The configuration UI is fairly straight forward. Select the certificate 71 | credential you created previously, supply the alias of the private key/certificate 72 | chain (optional if you have only one key entry), and finally supply the name or 73 | [Ant-style glob](https://ant.apache.org/manual/dirtasks.html) 74 | pattern specifying the APK files relative to the job workspace you want to sign. 75 | You can specify multiple glob patterns separated by commas if you wish. For most 76 | projects `**/*-unsigned.apk` should suffice. 77 | 78 | ![Sign Android APKs form](android-signing.png) 79 | 80 | You can tell a _Sign Android APKs_ build step the location of `zipalign` 81 | in the following ways, in order of precedence: 82 | 1. _Zipalign Path_ form input (expands environment variable references, e.g., `${CUSTOM_ANDROID_ZIPALIGN}`) 83 | 1. _ANDROID_HOME Override_ form input (expands environment variable references, e.g., `${CUSTOM_ANDROID_HOME}`) 84 | 1. `ANDROID_ZIPALIGN`[build variable](http://javadoc.jenkins-ci.org/hudson/model/AbstractBuild.html#getBuildVariables--) 85 | 1. `ANDROID_ZIPALIGN` [environment variable](http://javadoc.jenkins-ci.org/hudson/model/Run.html#getEnvironment-hudson.model.TaskListener-) 86 | 1. `ANDROID_HOME` build variable 87 | 1. `ANDROID_HOME` environment variable 88 | 1. `PATH` environment variable 89 | 1. A directory in `PATH` containing a file called `zipalign` 90 | 1. A directory in `PATH` that appears to be an Android SDK home, i.e., contains the `android` or `sdkmanager` utilities 91 | 92 | To access the first two override form parameters above, click the _Advanced_ button on the _Sign Android APKs_ 93 | build step form group. 94 | 95 | ![Sign Android APKs form](android-signing-advanced.png) 96 | 97 | Environment variables can come from various plugins, such as 98 | [Environment Injetor](https://wiki.jenkins-ci.org/display/JENKINS/EnvInject+Plugin) Plugin or 99 | [Custom Tools](https://plugins.jenkins.io/custom-tools-plugin) Plugin. The plugin searches 100 | an Android SDK home directory by finding the latest version under the `build-tools` directory 101 | installed in your SDK, and attempting to use the `zipalign` file that should be there, such 102 | as `${ANDROID_HOME}/build-tools/25.0.2/zipalign`. Hence. be sure your Android SDK has the 103 | _Build Tools_ package installed. I recommend setting up the SDK using the Custom Tools Plugin. 104 | To cover the Windows case, the plugin will search for `zipalign.exe` as well. 105 | 106 | Note that this plugin assumes your Android build has produced an unsigned, 107 | unaligned APK. If you are using the Gradle Android plugin to build your APK, 108 | that means a previous Jenkins build step probably invoked the `assembleRelease` 109 | task on your build script and there were no [`signingConfig`](https://developer.android.com/studio/publish/app-signing.html#gradle-sign) 110 | blocks that applied to your APK. In that case Gradle will have produced the 111 | necessary unsigned, unaligned APK, ready for the Android Signing Plugin to sign. 112 | 113 | ### Output Signed APKs 114 | 115 | As of version 2.2.0, there are two choices for the location where a _Sign Android APKs_ build 116 | step will write signed APKs. You can change this behavior by clicking the _Advanced_ 117 | button in the _Sign Android APKs_ step form group of a Freestyle job, and checking the desired 118 | radio button in the _Signed APK Destination_ group. 119 | * _Output to unsigned APK sibling_ - The new and default choice writes the signed APK to 120 | the same directory where the input unsigned APK resides, the same as the standard Android 121 | Gradle build would do. This option is useful when you want to use your Gradle build script 122 | to do something like publish the signed APK in a Gradle build step after your _Sign Android APKs_ 123 | build step runs. The standard Gradle Android plugin build should produce an unsigned APK 124 | named with the `-unsigned.apk` suffix. In that case, the _Android Signing Plugin_ plugin 125 | will simply remove the `-unsigned` component to create the signed APK file name. Otherwise, 126 | the plugin will insert `-signed` before `.apk` in the unsigned APK name. For example, 127 | `myApp-release-unsigned.apk` becomes `myApp-release.apk`, whereas `myApp-forElmo.apk` 128 | becomes `myApp-forElmo-signed.apk`. 129 | * _Output to separate directory_ - The original behavior writes signed APKs to a 130 | directory named like `SignApksBuilder-out/my-app-unsigned.apk/my-app-signed.apk`, 131 | where `my-app-unsigned.apk` is a directory named after the unsigned input APK. 132 | This is to avoid multiple signing steps in a single job overwriting each other's 133 | output APKs, and multiple APKs matched within a signing step colliding. It's 134 | clearly not fool-proof, however, so be mindful if you are signing multple APKs 135 | in a single job and/or signing step. 136 | 137 | Regardless of the output option you choose, if you use the plugin's 138 | _Archive Signed APKs_ and/or _Archive Unsigned APKs_ option, the plugin 139 | archives the artifacts under the `SignApksBuilder-out///my-app-unsigned.apk/` 140 | directory in the build's archive. 141 | 142 | ### Pipeline 143 | 144 | Here is an example of signing APKs from a [Pipeline](https://jenkins.io/doc/book/pipeline/) script: 145 | ``` 146 | node { 147 | // ... steps to build unsigned APK ... 148 | signAndroidApks ( 149 | keyStoreId: "myApp.signerKeyStore", 150 | keyAlias: "myTeam", 151 | apksToSign: "**/*-unsigned.apk" 152 | // uncomment the following line to output the signed APK to a separate directory as described above 153 | // signedApkMapping: [ $class: UnsignedApkBuilderDirMapping ] 154 | // uncomment the following line to output the signed APK as a sibling of the unsigned APK, as described above, or just omit signedApkMapping 155 | // you can override these within the script if necessary 156 | // androidHome: env.ANDROID_HOME 157 | // zipalignPath: env.ANDROID_ZIPALIGN 158 | ) 159 | } 160 | ``` 161 | Like the Free Style Job build step described above, the Pipeline step will attempt 162 | to use `ANDROID_ZIPALIGN` and `ANDROID_HOME`, in that priority order, from the 163 | Jenkins environment variables. Note the wrapping 164 | [`node`](https://jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#node-allocate-node) 165 | context; this plugin assumes the Pipeline step will have a workspace available. 166 | 167 | ### Job DSL 168 | 169 | This plugin offers a [Job DSL](https://github.com/jenkinsci/job-dsl-plugin/wiki) extension. 170 | You can include a _Sign Android APKs_ build step in the `steps` context of a Job DSL script: 171 | ``` 172 | freeStyleJob('myApp.seed') { 173 | scm { 174 | git 'git://github.com/mygithub/myApp.git', 'master', { 175 | extensions { 176 | relativeTragetDirectory 'myApp' 177 | } 178 | } 179 | } 180 | steps { 181 | gradle { 182 | rootBuildScriptDir 'myApp' 183 | useWrapper true 184 | tasks 'clean assembleRelease' 185 | } 186 | signAndroidApks '**/myApp-unsigned.apk', { 187 | keyStoreId 'myApp.keyStore' 188 | keyAlias 'myAppKey' 189 | // uncomment the following line to output the signed APK to a separate directory as described above 190 | // signedApkMapping unsignedApkNameDir() 191 | // uncomment the following line to output the signed APK as a sibling of the unsigned APK, as described above, or just omit signedApkMapping 192 | // signedApkMapping unsignedApkSibling() 193 | archiveSignedApks true 194 | archiveUnsignedApks true 195 | androidHome '/opt/android-sdk' 196 | } 197 | } 198 | } 199 | ``` 200 | The availble options are analogous to those in the build step configuration web UI. 201 | 202 | ## Support 203 | 204 | Please submit all issues to [Jenkins Jira](https://issues.jenkins-ci.org/issues/?jql=project%3DJENKINS%20AND%20component%3Dandroid-signing-plugin). 205 | Do not use GitHub issues. 206 | 207 | ## Release Notes 208 | 209 | See the [change log](CHANGELOG.md). 210 | 211 | ## License and Copyright 212 | 213 | See the included LICENSE and NOTICE text files for original Work and Derivative 214 | Work copyright and license information. 215 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/SignApksStepTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 4 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 5 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 6 | import org.junit.Before; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.rules.RuleChain; 10 | import org.jvnet.hudson.test.JenkinsRule; 11 | import org.jvnet.hudson.test.PretendSlave; 12 | 13 | import java.io.File; 14 | import java.net.URL; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | import hudson.EnvVars; 19 | import hudson.model.Run; 20 | import hudson.slaves.EnvironmentVariablesNodeProperty; 21 | 22 | import static org.hamcrest.CoreMatchers.endsWith; 23 | import static org.hamcrest.CoreMatchers.equalTo; 24 | import static org.hamcrest.CoreMatchers.hasItem; 25 | import static org.hamcrest.CoreMatchers.nullValue; 26 | import static org.hamcrest.CoreMatchers.startsWith; 27 | import static org.hamcrest.MatcherAssert.assertThat; 28 | import static org.junit.Assert.fail; 29 | 30 | 31 | public class SignApksStepTest { 32 | 33 | private JenkinsRule testJenkins = new JenkinsRule(); 34 | private TestKeyStore testKeyStore = new TestKeyStore(testJenkins); 35 | 36 | @Rule 37 | public RuleChain jenkinsChain = RuleChain.outerRule(testJenkins).around(testKeyStore); 38 | 39 | private String androidHome; 40 | private PretendSlave slave; 41 | private FakeZipalign zipalign; 42 | 43 | @Before 44 | public void setupEnvironment() throws Exception { 45 | URL androidHomeUrl = getClass().getResource("/android"); 46 | androidHome = new File(androidHomeUrl.toURI()).getAbsolutePath(); 47 | EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 48 | EnvVars envVars = prop.getEnvVars(); 49 | envVars.put("ANDROID_HOME", androidHome); 50 | testJenkins.jenkins.getGlobalNodeProperties().add(prop); 51 | zipalign = new FakeZipalign(); 52 | slave = testJenkins.createPretendSlave(zipalign); 53 | slave.getComputer().getEnvironment().put("ANDROID_HOME", androidHome); 54 | slave.setLabelString(slave.getLabelString() + " " + getClass().getSimpleName()); 55 | } 56 | 57 | @Test 58 | public void dslWorks() throws Exception { 59 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 60 | job.setDefinition(new CpsFlowDefinition(String.format( 61 | "node('%s') {%n" + 62 | " wrap($class: 'CopyTestWorkspace') {%n" + 63 | " signAndroidApks(" + 64 | " keyStoreId: '%s',%n" + 65 | " keyAlias: '%s',%n" + 66 | " apksToSign: '*-unsigned.apk, **/*-release-unsigned.apk',%n" + 67 | " archiveSignedApks: true,%n" + 68 | " archiveUnsignedApks: true,%n" + 69 | " androidHome: env.ANDROID_HOME%n" + 70 | " )%n" + 71 | " }%n" + 72 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS), false)); 73 | 74 | WorkflowRun build = testJenkins.buildAndAssertSuccess(job); 75 | List artifactNames = build.getArtifacts().stream().map(Run.Artifact::getFileName).collect(Collectors.toList()); 76 | 77 | assertThat(artifactNames.size(), equalTo(4)); 78 | assertThat(artifactNames, hasItem(endsWith("SignApksBuilderTest-unsigned.apk"))); 79 | assertThat(artifactNames, hasItem(endsWith("SignApksBuilderTest.apk"))); 80 | assertThat(artifactNames, hasItem(endsWith("app-release-unsigned.apk"))); 81 | assertThat(artifactNames, hasItem(endsWith("app-release.apk"))); 82 | assertThat(zipalign.lastProc.cmds().get(0), startsWith(androidHome)); 83 | } 84 | 85 | @Test 86 | public void setsAndroidHomeFromEnvVarsIfNotSpecifiedInScript() throws Exception { 87 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 88 | job.setDefinition(new CpsFlowDefinition(String.format( 89 | "node('%s') {%n" + 90 | " wrap($class: 'CopyTestWorkspace') {%n" + 91 | " signAndroidApks(" + 92 | " keyStoreId: '%s',%n" + 93 | " keyAlias: '%s',%n" + 94 | " apksToSign: '*-unsigned.apk, **/*-release-unsigned.apk'%n" + 95 | " )%n" + 96 | " }%n" + 97 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS), false)); 98 | 99 | WorkflowRun build = testJenkins.buildAndAssertSuccess(job); 100 | List artifactNames = build.getArtifacts().stream().map(Run.Artifact::getFileName).collect(Collectors.toList()); 101 | 102 | assertThat(artifactNames.size(), equalTo(2)); 103 | assertThat(artifactNames, hasItem(endsWith("SignApksBuilderTest.apk"))); 104 | assertThat(artifactNames, hasItem(endsWith("app-release.apk"))); 105 | assertThat(zipalign.lastProc.cmds().get(0), startsWith(androidHome)); 106 | } 107 | 108 | @Test 109 | public void setsAndroidZipalignFromEnvVarsIfNotSpecifiedInScript() throws Exception { 110 | URL altZipalignUrl = getClass().getResource("/alt-zipalign/zipalign"); 111 | String altZipalign = new File(altZipalignUrl.toURI()).getAbsolutePath(); 112 | EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 113 | EnvVars envVars = prop.getEnvVars(); 114 | envVars.put("ANDROID_ZIPALIGN", altZipalign); 115 | 116 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 117 | job.setDefinition(new CpsFlowDefinition(String.format( 118 | "node('%s') {%n" + 119 | " wrap($class: 'CopyTestWorkspace') {%n" + 120 | " signAndroidApks(" + 121 | " keyStoreId: '%s',%n" + 122 | " keyAlias: '%s',%n" + 123 | " apksToSign: '**/*-unsigned.apk'%n" + 124 | " )%n" + 125 | " }%n" + 126 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS), false)); 127 | 128 | WorkflowRun build = testJenkins.buildAndAssertSuccess(job); 129 | List artifactNames = build.getArtifacts().stream().map(Run.Artifact::getFileName).collect(Collectors.toList()); 130 | 131 | assertThat(artifactNames.size(), equalTo(3)); 132 | assertThat(artifactNames, hasItem(endsWith("SignApksBuilderTest.apk"))); 133 | assertThat(artifactNames, hasItem(endsWith("app-release.apk"))); 134 | assertThat(artifactNames, hasItem(endsWith("app-debug.apk"))); 135 | assertThat(zipalign.lastProc.cmds().get(0), startsWith(androidHome)); 136 | } 137 | 138 | @Test 139 | public void doesNotUseEnvVarsIfScriptSpecifiesAndroidHomeOrZipalign() throws Exception { 140 | URL altAndroidHomeUrl = getClass().getResource("/win-android"); 141 | String altAndroidHome = new File(altAndroidHomeUrl.toURI()).getAbsolutePath(); 142 | 143 | EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); 144 | EnvVars envVars = prop.getEnvVars(); 145 | envVars.put("ANDROID_ZIPALIGN", "/fail/zipalign"); 146 | testJenkins.jenkins.getGlobalNodeProperties().add(prop); 147 | 148 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 149 | job.setDefinition(new CpsFlowDefinition(String.format( 150 | "node('%s') {%n" + 151 | " wrap($class: 'CopyTestWorkspace') {%n" + 152 | " signAndroidApks(" + 153 | " keyStoreId: '%s',%n" + 154 | " keyAlias: '%s',%n" + 155 | " apksToSign: '**/*-unsigned.apk',%n" + 156 | " androidHome: '%s'%n" + 157 | " )%n" + 158 | " }%n" + 159 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS, altAndroidHome.replace("\\", "\\\\")), false)); 160 | 161 | testJenkins.buildAndAssertSuccess(job); 162 | 163 | assertThat(zipalign.lastProc.cmds().get(0), startsWith(altAndroidHome)); 164 | 165 | URL altZipalignUrl = getClass().getResource("/alt-zipalign/zipalign"); 166 | String altZipalign = new File(altZipalignUrl.toURI()).getAbsolutePath(); 167 | 168 | job.setDefinition(new CpsFlowDefinition(String.format( 169 | "node('%s') {%n" + 170 | " wrap($class: 'CopyTestWorkspace') {%n" + 171 | " signAndroidApks(" + 172 | " keyStoreId: '%s',%n" + 173 | " keyAlias: '%s',%n" + 174 | " apksToSign: '**/*-unsigned.apk',%n" + 175 | " zipalignPath: '%s'%n" + 176 | " )%n" + 177 | " }%n" + 178 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS, altZipalign.replace("\\", "\\\\")), false)); 179 | 180 | testJenkins.buildAndAssertSuccess(job); 181 | 182 | assertThat(zipalign.lastProc.cmds().get(0), startsWith(altZipalign)); 183 | } 184 | 185 | @Test 186 | public void skipsZipalign() throws Exception { 187 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 188 | job.setDefinition(new CpsFlowDefinition(String.format( 189 | "node('%s') {%n" + 190 | " wrap($class: 'CopyTestWorkspace') {%n" + 191 | " signAndroidApks(" + 192 | " keyStoreId: '%s',%n" + 193 | " keyAlias: '%s',%n" + 194 | " apksToSign: '**/*-unsigned.apk',%n" + 195 | " skipZipalign: true%n" + 196 | " )%n" + 197 | " }%n" + 198 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS), false)); 199 | 200 | testJenkins.buildAndAssertSuccess(job); 201 | 202 | assertThat(zipalign.lastProc, nullValue()); 203 | } 204 | 205 | @Test 206 | public void signedApkMappingDefaultsToUnsignedApkSibling() throws Exception { 207 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 208 | job.setDefinition(new CpsFlowDefinition(String.format( 209 | "node('%s') {%n" + 210 | " wrap($class: 'CopyTestWorkspace') {%n" + 211 | " signAndroidApks(" + 212 | " keyStoreId: '%s',%n" + 213 | " keyAlias: '%s',%n" + 214 | " apksToSign: 'SignApksBuilderTest-unsigned.apk',%n" + 215 | " archiveSignedApks: false%n" + 216 | " )%n" + 217 | " archive includes: 'SignApksBuilderTest.apk'%n" + 218 | " }%n" + 219 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS), false)); 220 | 221 | WorkflowRun run = testJenkins.buildAndAssertSuccess(job); 222 | List artifacts = run.getArtifacts(); 223 | 224 | assertThat(artifacts.size(), equalTo(1)); 225 | assertThat(artifacts.get(0).getFileName(), equalTo("SignApksBuilderTest.apk")); 226 | } 227 | 228 | @Test 229 | public void usesSpecifiedSignedApkMapping() throws Exception { 230 | WorkflowJob job = testJenkins.jenkins.createProject(WorkflowJob.class, getClass().getSimpleName()); 231 | job.setDefinition(new CpsFlowDefinition(String.format( 232 | "node('%s') {%n" + 233 | " wrap($class: 'CopyTestWorkspace') {%n" + 234 | " signAndroidApks(" + 235 | " keyStoreId: '%s',%n" + 236 | " keyAlias: '%s',%n" + 237 | " apksToSign: 'SignApksBuilderTest-unsigned.apk',%n" + 238 | " archiveSignedApks: false,%n" + 239 | " signedApkMapping: [$class: 'TestSignedApkMapping']%n" + 240 | " )%n" + 241 | " archive includes: 'TestSignedApkMapping-SignApksBuilderTest-unsigned.apk'%n" + 242 | " }%n" + 243 | "}", getClass().getSimpleName(), TestKeyStore.KEY_STORE_ID, TestKeyStore.KEY_ALIAS), false)); 244 | 245 | WorkflowRun run = testJenkins.buildAndAssertSuccess(job); 246 | List artifacts = run.getArtifacts(); 247 | 248 | assertThat(artifacts.size(), equalTo(1)); 249 | assertThat(artifacts.get(0).getFileName(), equalTo("TestSignedApkMapping-SignApksBuilderTest-unsigned.apk")); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/androidsigning/ZipalignToolTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.androidsigning; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.rules.TemporaryFolder; 8 | import org.junit.rules.TestName; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.net.URISyntaxException; 13 | import java.net.URL; 14 | import java.nio.charset.Charset; 15 | 16 | import hudson.EnvVars; 17 | import hudson.FilePath; 18 | import hudson.Util; 19 | import hudson.util.ArgumentListBuilder; 20 | 21 | import static org.hamcrest.CoreMatchers.startsWith; 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | 24 | 25 | public class ZipalignToolTest { 26 | 27 | FilePath workspace; 28 | FilePath androidHome; 29 | FilePath androidHomeZipalign; 30 | FilePath altZipalign; 31 | 32 | @Rule 33 | public TemporaryFolder tempDir = new TemporaryFolder(); 34 | 35 | @Before 36 | public void copyWorkspace() throws Exception { 37 | FilePath tempDirPath = new FilePath(tempDir.getRoot()); 38 | 39 | URL workspaceUrl = getClass().getResource("/workspace"); 40 | FilePath workspace = new FilePath(new File(workspaceUrl.toURI())); 41 | this.workspace = tempDirPath.child("workspace"); 42 | workspace.copyRecursiveTo(this.workspace); 43 | 44 | URL androidHomeUrl = getClass().getResource("/android"); 45 | FilePath androidHome = new FilePath(new File(androidHomeUrl.toURI())); 46 | this.androidHome = tempDirPath.child("android-sdk"); 47 | androidHome.copyRecursiveTo(this.androidHome); 48 | androidHomeZipalign = this.androidHome.child("build-tools").child("1.0").child("zipalign"); 49 | 50 | URL altZipalignUrl = getClass().getResource("/alt-zipalign"); 51 | FilePath altZipalign = new FilePath(new File(altZipalignUrl.toURI())); 52 | this.altZipalign = tempDirPath.child("alt-zipalign"); 53 | altZipalign.copyRecursiveTo(this.altZipalign); 54 | this.altZipalign = this.altZipalign.child("zipalign"); 55 | } 56 | 57 | @Test 58 | public void findsZipalignInAndroidHomeEnvVar() throws Exception { 59 | EnvVars envVars = new EnvVars(); 60 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, androidHome.getRemote()); 61 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 62 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 63 | 64 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 65 | } 66 | 67 | @Test 68 | public void findsZipalignInAndroidZipalignEnvVar() throws Exception { 69 | EnvVars envVars = new EnvVars(); 70 | envVars.put(ZipalignTool.ENV_ZIPALIGN_PATH, altZipalign.getRemote()); 71 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 72 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 73 | 74 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 75 | } 76 | 77 | @Test 78 | public void findsZipalignInPathEnvVarWithToolsDir() throws Exception { 79 | FilePath toolsDir = androidHome.child("tools"); 80 | toolsDir.mkdirs(); 81 | FilePath androidTool = toolsDir.child("android"); 82 | androidTool.write(getClass().getSimpleName(), "utf-8"); 83 | 84 | EnvVars envVars = new EnvVars(); 85 | 86 | String path = String.join(File.pathSeparator, toolsDir.getRemote(), "/other/tools", "/other/bin"); 87 | envVars.put(ZipalignTool.ENV_PATH, path); 88 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 89 | ArgumentListBuilder cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 90 | 91 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 92 | 93 | path = String.join(File.pathSeparator, "/other/tools", toolsDir.getRemote(), "/other/bin"); 94 | envVars.put(ZipalignTool.ENV_PATH, path); 95 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 96 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 97 | 98 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 99 | 100 | path = String.join(File.pathSeparator, "/other/tools", "/other/bin", toolsDir.getRemote()); 101 | envVars.put(ZipalignTool.ENV_PATH, path); 102 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 103 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 104 | 105 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 106 | 107 | path = String.join(File.pathSeparator, toolsDir.getRemote()); 108 | envVars.put(ZipalignTool.ENV_PATH, path); 109 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 110 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 111 | 112 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 113 | } 114 | 115 | @Test 116 | public void findsZipalignInPathEnvVarWithToolsBinDir() throws Exception { 117 | FilePath toolsBinDir = androidHome.child("tools").child("bin"); 118 | toolsBinDir.mkdirs(); 119 | FilePath sdkmanagerTool = toolsBinDir.child("sdkmanager"); 120 | sdkmanagerTool.write(getClass().getSimpleName(), "utf-8"); 121 | 122 | EnvVars envVars = new EnvVars(); 123 | 124 | String path = String.join(File.pathSeparator, toolsBinDir.getRemote(), "/other/tools", "/other/bin"); 125 | envVars.put(ZipalignTool.ENV_PATH, path); 126 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 127 | ArgumentListBuilder cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 128 | 129 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 130 | 131 | path = String.join(File.pathSeparator, "/other/tools", toolsBinDir.getRemote(), "/other/bin"); 132 | envVars.put(ZipalignTool.ENV_PATH, path); 133 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 134 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 135 | 136 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 137 | 138 | path = String.join(File.pathSeparator, "/other/tools", "/other/bin", toolsBinDir.getRemote()); 139 | envVars.put(ZipalignTool.ENV_PATH, path); 140 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 141 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 142 | 143 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 144 | 145 | path = String.join(File.pathSeparator, toolsBinDir.getRemote()); 146 | envVars.put(ZipalignTool.ENV_PATH, path); 147 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 148 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 149 | 150 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 151 | } 152 | 153 | @Test 154 | public void findsZipalignInPathEnvVarWithZipalignParentDir() throws Exception { 155 | FilePath zipalignDir = altZipalign.getParent(); 156 | 157 | EnvVars envVars = new EnvVars(); 158 | 159 | String path = String.join(File.pathSeparator, zipalignDir.getRemote(), "/other/tools", "/other/bin"); 160 | envVars.put(ZipalignTool.ENV_PATH, path); 161 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 162 | ArgumentListBuilder cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 163 | 164 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 165 | 166 | path = String.join(File.pathSeparator, "/other/tools", zipalignDir.getRemote(), "/other/bin"); 167 | envVars.put(ZipalignTool.ENV_PATH, path); 168 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 169 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 170 | 171 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 172 | 173 | path = String.join(File.pathSeparator, "/other/tools", "/other/bin", zipalignDir.getRemote()); 174 | envVars.put(ZipalignTool.ENV_PATH, path); 175 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 176 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 177 | 178 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 179 | 180 | path = String.join(File.pathSeparator, zipalignDir.getRemote()); 181 | envVars.put(ZipalignTool.ENV_PATH, path); 182 | zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 183 | cmd = zipalign.commandFor("path-test.apk", "path-test-aligned.apk"); 184 | 185 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 186 | } 187 | 188 | @Test 189 | public void androidZiplignOverridesAndroidHome() throws Exception { 190 | EnvVars envVars = new EnvVars(); 191 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, androidHomeZipalign.getRemote()); 192 | envVars.put(ZipalignTool.ENV_ZIPALIGN_PATH, altZipalign.getRemote()); 193 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 194 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 195 | 196 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 197 | } 198 | 199 | @Test 200 | public void usesLatestZipalignFromAndroidHome() throws IOException, InterruptedException { 201 | FilePath newerBuildTools = androidHome.child("build-tools").child("1.1"); 202 | newerBuildTools.mkdirs(); 203 | FilePath newerZipalign = newerBuildTools.child("zipalign"); 204 | newerZipalign.write("# fake zipalign", "utf-8"); 205 | 206 | EnvVars envVars = new EnvVars(); 207 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, androidHome.getRemote()); 208 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 209 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 210 | 211 | assertThat(cmd.toString(), startsWith(newerZipalign.getRemote())); 212 | 213 | newerBuildTools.deleteRecursive(); 214 | } 215 | 216 | @Test 217 | public void explicitAndroidHomeOverridesEnvVars() throws Exception { 218 | EnvVars envVars = new EnvVars(); 219 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, androidHomeZipalign.getRemote()); 220 | envVars.put(ZipalignTool.ENV_ZIPALIGN_PATH, altZipalign.getRemote()); 221 | 222 | FilePath explicitAndroidHome = workspace.createTempDir("my-android-home", ""); 223 | androidHome.copyRecursiveTo(explicitAndroidHome); 224 | 225 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, explicitAndroidHome.getRemote(), null); 226 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 227 | 228 | assertThat(cmd.toString(), startsWith(explicitAndroidHome.getRemote())); 229 | 230 | explicitAndroidHome.deleteRecursive(); 231 | } 232 | 233 | @Test 234 | public void explicitZipalignOverridesEnvZipaligns() throws IOException, InterruptedException { 235 | EnvVars envVars = new EnvVars(); 236 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, androidHomeZipalign.getRemote()); 237 | envVars.put(ZipalignTool.ENV_ZIPALIGN_PATH, altZipalign.getRemote()); 238 | 239 | FilePath explicitZipalign = workspace.createTempDir("my-zipalign", "").child("zipalign"); 240 | explicitZipalign.write("# fake zipalign", "utf-8"); 241 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, explicitZipalign.getRemote()); 242 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 243 | 244 | assertThat(cmd.toString(), startsWith(explicitZipalign.getRemote())); 245 | 246 | explicitZipalign.getParent().deleteRecursive(); 247 | } 248 | 249 | @Test 250 | public void explicitZipalignOverridesEverything() throws Exception { 251 | EnvVars envVars = new EnvVars(); 252 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, androidHomeZipalign.getRemote()); 253 | envVars.put(ZipalignTool.ENV_ZIPALIGN_PATH, altZipalign.getRemote()); 254 | 255 | FilePath explicitAndroidHome = workspace.createTempDir("my-android-home", ""); 256 | androidHome.copyRecursiveTo(explicitAndroidHome); 257 | 258 | FilePath explicitZipalign = workspace.createTempDir("my-zipalign", "").child("zipalign"); 259 | explicitZipalign.write("# fake zipalign", "utf-8"); 260 | 261 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, explicitAndroidHome.getRemote(), explicitZipalign.getRemote()); 262 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 263 | 264 | assertThat(cmd.toString(), startsWith(explicitZipalign.getRemote())); 265 | 266 | explicitZipalign.getParent().deleteRecursive(); 267 | explicitAndroidHome.deleteRecursive(); 268 | } 269 | 270 | @Test 271 | public void triesWindowsExeIfEnvAndroidHomeZipalignDoesNotExist() throws IOException, InterruptedException, URISyntaxException { 272 | URL androidHomeUrl = getClass().getResource("/win-android"); 273 | FilePath winAndroidHome = new FilePath(new File(androidHomeUrl.toURI())); 274 | FilePath winAndroidHomeZipalign = winAndroidHome.child("build-tools").child("1.0").child("zipalign.exe"); 275 | 276 | EnvVars envVars = new EnvVars(); 277 | envVars.put(ZipalignTool.ENV_ANDROID_HOME, winAndroidHome.getRemote()); 278 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 279 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 280 | 281 | assertThat(cmd.toString(), startsWith(winAndroidHomeZipalign.getRemote())); 282 | } 283 | 284 | @Test 285 | public void triesWindowsExeIfEnvZipalignDoesNotExist() throws Exception { 286 | URL androidHomeUrl = getClass().getResource("/win-android"); 287 | FilePath winAndroidHome = new FilePath(new File(androidHomeUrl.toURI())); 288 | FilePath suffixedZipalign = winAndroidHome.child("build-tools").child("1.0").child("zipalign.exe"); 289 | FilePath unsuffixedZipalign = suffixedZipalign.getParent().child("zipalign"); 290 | 291 | EnvVars envVars = new EnvVars(); 292 | envVars.put(ZipalignTool.ENV_ZIPALIGN_PATH, unsuffixedZipalign.getRemote()); 293 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, null); 294 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 295 | 296 | assertThat(cmd.toString(), startsWith(suffixedZipalign.getRemote())); 297 | } 298 | 299 | @Test 300 | public void triesWindowsExeIfExplicitAndroidHomeZipalignDoesNotExist() throws Exception { 301 | URL androidHomeUrl = getClass().getResource("/win-android"); 302 | FilePath winAndroidHome = new FilePath(new File(androidHomeUrl.toURI())); 303 | FilePath winAndroidHomeZipalign = winAndroidHome.child("build-tools").child("1.0").child("zipalign.exe"); 304 | 305 | EnvVars envVars = new EnvVars(); 306 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, winAndroidHome.getRemote(), null); 307 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 308 | 309 | assertThat(cmd.toString(), startsWith(winAndroidHomeZipalign.getRemote())); 310 | } 311 | 312 | @Test 313 | public void triesWindowsExeIfExplicitZipalignDoesNotExist() throws Exception { 314 | URL androidHomeUrl = getClass().getResource("/win-android"); 315 | FilePath winAndroidHome = new FilePath(new File(androidHomeUrl.toURI())); 316 | FilePath suffixedZipalign = winAndroidHome.child("build-tools").child("1.0").child("zipalign.exe"); 317 | FilePath unsuffixedZipalign = suffixedZipalign.getParent().child("zipalign"); 318 | 319 | EnvVars envVars = new EnvVars(); 320 | ZipalignTool zipalign = new ZipalignTool(envVars, workspace, System.out, null, unsuffixedZipalign.getRemote()); 321 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 322 | 323 | assertThat(cmd.toString(), startsWith(suffixedZipalign.getRemote())); 324 | } 325 | 326 | @Test 327 | public void resolvesVariableReferencesInExplicitParameters() throws Exception { 328 | EnvVars env = new EnvVars(); 329 | env.put("ALT_ZIPALIGN", altZipalign.getRemote()); 330 | ZipalignTool zipalign = new ZipalignTool(env, workspace, System.out, null, "${ALT_ZIPALIGN}"); 331 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 332 | 333 | assertThat(cmd.toString(), startsWith(altZipalign.getRemote())); 334 | 335 | env.clear(); 336 | env.put("ALT_ANDROID_HOME", androidHome.getRemote()); 337 | zipalign = new ZipalignTool(env, workspace, System.out, "${ALT_ANDROID_HOME}", null); 338 | cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 339 | 340 | assertThat(cmd.toString(), startsWith(androidHomeZipalign.getRemote())); 341 | } 342 | 343 | @Test 344 | public void findsWindowsZipalignFromEnvPath() throws Exception { 345 | URL url = getClass().getResource("/win-android"); 346 | FilePath winAndroidHome = new FilePath(new File(url.toURI())); 347 | EnvVars env = new EnvVars(); 348 | env.put("PATH", winAndroidHome.getRemote()); 349 | 350 | ZipalignTool zipalign = new ZipalignTool(env, workspace, System.out, null, null); 351 | ArgumentListBuilder cmd = zipalign.commandFor("test.apk", "test-aligned.apk"); 352 | 353 | assertThat(cmd.toString(), startsWith(winAndroidHome.getRemote())); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/androidsigning/SignApksBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | =========================================================== 3 | Apache License, Version 2.0 - Derivative Work modified file 4 | ----------------------------------------------------------- 5 | This file has been modified by BIT Systems. 6 | All modifications are copyright (c) BIT Systems, 2016. 7 | =========================================================== 8 | 9 | This file was originally named SignArtifactsPlugin.java. The contents of this 10 | file have been signifcantly modified from the original Work contents. 11 | */ 12 | 13 | package org.jenkinsci.plugins.androidsigning; 14 | 15 | import com.android.apksig.ApkSigner; 16 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 17 | import com.cloudbees.plugins.credentials.CredentialsProvider; 18 | import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; 19 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 20 | 21 | import org.apache.commons.lang.StringUtils; 22 | import org.jenkinsci.Symbol; 23 | import org.kohsuke.stapler.AncestorInPath; 24 | import org.kohsuke.stapler.DataBoundConstructor; 25 | import org.kohsuke.stapler.DataBoundSetter; 26 | import org.kohsuke.stapler.QueryParameter; 27 | 28 | import java.io.File; 29 | import java.io.IOException; 30 | import java.io.PrintWriter; 31 | import java.net.URI; 32 | import java.security.GeneralSecurityException; 33 | import java.security.PrivateKey; 34 | import java.security.cert.Certificate; 35 | import java.security.cert.X509Certificate; 36 | import java.util.ArrayList; 37 | import java.util.Arrays; 38 | import java.util.Collections; 39 | import java.util.Comparator; 40 | import java.util.LinkedHashMap; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.Set; 44 | import java.util.TreeSet; 45 | 46 | import edu.umd.cs.findbugs.annotations.NonNull; 47 | 48 | import hudson.AbortException; 49 | import hudson.EnvVars; 50 | import hudson.Extension; 51 | import hudson.FilePath; 52 | import hudson.Launcher; 53 | import hudson.model.AbstractBuild; 54 | import hudson.model.AbstractProject; 55 | import hudson.model.Item; 56 | import hudson.model.ItemGroup; 57 | import hudson.model.Result; 58 | import hudson.model.Run; 59 | import hudson.model.TaskListener; 60 | import hudson.remoting.VirtualChannel; 61 | import hudson.security.ACL; 62 | import hudson.tasks.BuildStepDescriptor; 63 | import hudson.tasks.Builder; 64 | import hudson.util.ArgumentListBuilder; 65 | import hudson.util.FormValidation; 66 | import hudson.util.ListBoxModel; 67 | import jenkins.MasterToSlaveFileCallable; 68 | import jenkins.model.Jenkins; 69 | import jenkins.tasks.SimpleBuildStep; 70 | import jenkins.util.BuildListenerAdapter; 71 | 72 | public class SignApksBuilder extends Builder implements SimpleBuildStep { 73 | 74 | static final List NO_REQUIREMENTS = Collections.emptyList(); 75 | static final String BUILDER_DIR = SignApksBuilder.class.getSimpleName() + "-out"; 76 | 77 | static List singleEntryBuildersFromEntriesOfBuilder(SignApksBuilder oldBuilder) { 78 | List signers = new ArrayList<>(oldBuilder.getEntries().size()); 79 | for (Apk apk : oldBuilder.getEntries()) { 80 | SignApksBuilder b = new SignApksBuilder(); 81 | b.setPropertiesFromOldSigningEntry(apk); 82 | b.setAndroidHome(oldBuilder.getAndroidHome()); 83 | b.setZipalignPath(oldBuilder.getZipalignPath()); 84 | signers.add(b); 85 | } 86 | return signers; 87 | } 88 | 89 | private static String[] getSelectionGlobs(String apksToSignValue) { 90 | String[] globs = apksToSignValue.split("\\s*,\\s*"); 91 | List cleanGlobs = new ArrayList<>(globs.length); 92 | for (String glob : globs) { 93 | glob = glob.trim(); 94 | if (glob.length() > 0) { 95 | cleanGlobs.add(glob); 96 | } 97 | } 98 | return cleanGlobs.toArray(new String[cleanGlobs.size()]); 99 | } 100 | 101 | private String androidHome; 102 | private String zipalignPath; 103 | private String keyStoreId; 104 | private String keyAlias; 105 | private String apksToSign; 106 | private SignedApkMappingStrategy signedApkMapping; 107 | private boolean archiveSignedApks = true; 108 | private boolean archiveUnsignedApks = false; 109 | private boolean skipZipalign = false; 110 | 111 | transient private List entries; 112 | 113 | @Deprecated 114 | public SignApksBuilder(List entries) { 115 | this(); 116 | if (entries.size() == 1) { 117 | setPropertiesFromOldSigningEntry(entries.get(0)); 118 | } 119 | else if (entries.size() > 1) { 120 | throw new UnsupportedOperationException("this constructor is deprecated; use multiple build steps instead of multiple signing entries"); 121 | } 122 | } 123 | 124 | @DataBoundConstructor 125 | public SignApksBuilder() { 126 | signedApkMapping = new SignedApkMappingStrategy.UnsignedApkSiblingMapping(); 127 | } 128 | 129 | protected Object readResolve() { 130 | if (signedApkMapping == null) { 131 | signedApkMapping = new SignedApkMappingStrategy.UnsignedApkBuilderDirMapping(); 132 | } 133 | return this; 134 | } 135 | 136 | private void setPropertiesFromOldSigningEntry(Apk entry) { 137 | setKeyStoreId(entry.getKeyStore()); 138 | setKeyAlias(entry.getAlias()); 139 | setApksToSign(entry.getApksToSign()); 140 | setSignedApkMapping(new SignedApkMappingStrategy.UnsignedApkBuilderDirMapping()); 141 | setArchiveSignedApks(entry.getArchiveSignedApks()); 142 | setArchiveUnsignedApks(entry.getArchiveUnsignedApks()); 143 | } 144 | 145 | private boolean isIntermediateFailure(Run build) { 146 | // TODO: does this work in pipeline? 147 | Result result = build.getResult(); 148 | return result != null && result.isWorseThan(Result.UNSTABLE); 149 | } 150 | 151 | boolean isMigrated() { 152 | return entries == null || entries.isEmpty(); 153 | } 154 | 155 | @Deprecated 156 | public List getEntries() { 157 | return entries; 158 | } 159 | 160 | @DataBoundSetter 161 | public void setAndroidHome(String x) { 162 | androidHome = StringUtils.stripToNull(x); 163 | } 164 | 165 | public String getAndroidHome() { 166 | return androidHome; 167 | } 168 | 169 | @DataBoundSetter 170 | public void setZipalignPath(String x) { 171 | zipalignPath = StringUtils.stripToNull(x); 172 | } 173 | 174 | public String getZipalignPath() { 175 | return zipalignPath; 176 | } 177 | 178 | @DataBoundSetter 179 | public void setKeyStoreId(String x) { 180 | keyStoreId = x; 181 | } 182 | 183 | public String getKeyStoreId() { 184 | return keyStoreId; 185 | } 186 | 187 | @DataBoundSetter 188 | public void setKeyAlias(String x) { 189 | keyAlias = x; 190 | } 191 | 192 | public String getKeyAlias() { 193 | return keyAlias; 194 | } 195 | 196 | @DataBoundSetter 197 | public void setApksToSign(String x) { 198 | apksToSign = x; 199 | } 200 | 201 | public String getApksToSign() { 202 | return apksToSign; 203 | } 204 | 205 | @DataBoundSetter 206 | public void setSignedApkMapping(SignedApkMappingStrategy x) { 207 | signedApkMapping = x; 208 | } 209 | 210 | public SignedApkMappingStrategy getSignedApkMapping() { 211 | return signedApkMapping; 212 | } 213 | 214 | @DataBoundSetter 215 | public void setSkipZipalign(boolean x) { 216 | skipZipalign = x; 217 | } 218 | 219 | public boolean getSkipZipalign() { 220 | return skipZipalign; 221 | } 222 | 223 | @DataBoundSetter 224 | public void setArchiveSignedApks(boolean x) { 225 | archiveSignedApks = x; 226 | } 227 | 228 | public boolean getArchiveSignedApks() { 229 | return archiveSignedApks; 230 | } 231 | 232 | @DataBoundSetter 233 | public void setArchiveUnsignedApks(boolean x) { 234 | archiveUnsignedApks = x; 235 | } 236 | 237 | public boolean getArchiveUnsignedApks() { 238 | return archiveUnsignedApks; 239 | } 240 | 241 | @Override 242 | public void perform(@NonNull Run run, @NonNull FilePath workspace, @NonNull Launcher launcher, @NonNull TaskListener listener) throws InterruptedException, IOException { 243 | if (isIntermediateFailure(run)) { 244 | listener.getLogger().println("[SignApksBuilder] skipping Sign APKs step because a previous step failed"); 245 | return; 246 | } 247 | 248 | if (getEntries() != null && !getEntries().isEmpty()) { 249 | List newModelBuilders = singleEntryBuildersFromEntriesOfBuilder(this); 250 | for (SignApksBuilder builder : newModelBuilders) { 251 | builder.perform(run, workspace, launcher, listener); 252 | } 253 | return; 254 | } 255 | 256 | ArgumentListBuilder command = new ArgumentListBuilder().add("echo").addQuoted("resolving effective environment"); 257 | command.toWindowsCommand(); 258 | if (!launcher.isUnix()) { 259 | command = command.toWindowsCommand(); 260 | } 261 | // force the Custom Tools plugin to inject the custom tools env vars via its DecoratedLauncher 262 | Launcher.ProcStarter getEffectiveEnv = launcher.launch().pwd(workspace).cmds(command); 263 | try { 264 | getEffectiveEnv.join(); 265 | } 266 | catch (Exception e) { 267 | listener.getLogger().println("[SignApksBuilder] error resolving effective script environment, but this does not necessarily fail your build:"); 268 | e.printStackTrace(listener.getLogger()); 269 | } 270 | String[] envLines = getEffectiveEnv.envs(); 271 | EnvVars shellEnv = new EnvVars(); 272 | for (String envVar : envLines) { 273 | shellEnv.addLine(envVar); 274 | } 275 | 276 | EnvVars env = new EnvVars(); 277 | if (run instanceof AbstractBuild) { 278 | EnvVars runEnv = run.getEnvironment(listener); 279 | env.overrideExpandingAll(runEnv); 280 | env.overrideExpandingAll(((AbstractBuild) run).getBuildVariables()); 281 | } 282 | env.overrideAll(shellEnv); 283 | 284 | FilePath builderDir = workspace.child(BUILDER_DIR); 285 | FilePath zipalignDir = builderDir.child("zipalign"); 286 | zipalignDir.mkdirs(); 287 | 288 | ZipalignTool zipalign = new ZipalignTool(env, workspace, listener.getLogger(), androidHome, zipalignPath); 289 | Map apksToArchive = new LinkedHashMap<>(); 290 | 291 | StandardCertificateCredentials keyStoreCredential = getKeystore(getKeyStoreId(), run.getParent()); 292 | SigningComponents signingParams; 293 | try { 294 | signingParams = SigningComponents.fromCredentials(keyStoreCredential, getKeyAlias()); 295 | } 296 | catch (GeneralSecurityException e) { 297 | String message = "Error reading signing key from key store credential " + keyStoreCredential.getId() + ": " + e.getMessage(); 298 | listener.fatalError(message); 299 | e.printStackTrace(listener.getLogger()); 300 | throw new AbortException(message); 301 | } 302 | 303 | Set matchedApks = new TreeSet<>(Comparator.comparing(FilePath::getRemote)); 304 | String[] globs = getSelectionGlobs(getApksToSign()); 305 | for (String glob : globs) { 306 | FilePath[] globMatch = workspace.list(glob, builderDir.getName() + "/**"); 307 | if (globMatch.length == 0) { 308 | throw new AbortException("No APKs in workspace matching " + glob); 309 | } 310 | matchedApks.addAll(Arrays.asList(globMatch)); 311 | } 312 | 313 | final String archivePrefix = BUILDER_DIR + "/" + getKeyStoreId() + "/" + getKeyAlias() + "/"; 314 | 315 | if (signedApkMapping == null) { 316 | signedApkMapping = new SignedApkMappingStrategy.UnsignedApkSiblingMapping(); 317 | } 318 | 319 | for (FilePath unsignedApk : matchedApks) { 320 | unsignedApk = unsignedApk.absolutize(); 321 | 322 | FilePath alignedApk = zipalignDir.createTempFile("aligned-" + unsignedApk.getBaseName() + "-", ".apk"); 323 | FilePath signedApk = signedApkMapping.destinationForUnsignedApk(unsignedApk, workspace); 324 | 325 | if (skipZipalign) { 326 | listener.getLogger().printf("[SignApksBuilder] skipping zipalign for unsigned apk %s", unsignedApk); 327 | alignedApk = unsignedApk; 328 | } 329 | else { 330 | ArgumentListBuilder zipalignCommand = zipalign.commandFor(unsignedApk.getRemote(), alignedApk.getRemote()); 331 | listener.getLogger().printf("[SignApksBuilder] %s%n", zipalignCommand); 332 | int zipalignResult = launcher.launch() 333 | .cmds(zipalignCommand) 334 | .pwd(workspace) 335 | .stdout(listener) 336 | .stderr(listener.getLogger()) 337 | .join(); 338 | 339 | if (zipalignResult != 0) { 340 | listener.fatalError("[SignApksBuilder] zipalign failed: exit code %d", zipalignResult); 341 | throw new AbortException(String.format("zipalign failed on APK %s: exit code %d", unsignedApk, zipalignResult)); 342 | } 343 | } 344 | 345 | String alignedRelName = relativeToWorkspace(workspace, alignedApk); 346 | String signedRelName = relativeToWorkspace(workspace, signedApk); 347 | 348 | if (!alignedApk.exists()) { 349 | throw new AbortException(String.format("aligned APK does not exist: %s", alignedRelName)); 350 | } 351 | 352 | listener.getLogger().printf("[SignApksBuilder] signing APK %s%n", alignedRelName); 353 | 354 | FilePath signedParent = signedApk.getParent(); 355 | if (signedParent == null) { 356 | continue; 357 | } 358 | if (!signedParent.exists()) { 359 | signedParent.mkdirs(); 360 | } 361 | SignApkCallable signApk = new SignApkCallable(signingParams.key, signingParams.certChain, signingParams.v1SigName, signedApk.getRemote(), listener); 362 | alignedApk.act(signApk); 363 | 364 | listener.getLogger().printf("[SignApksBuilder] signed APK %s%n", signedRelName); 365 | 366 | if (getArchiveUnsignedApks()) { 367 | listener.getLogger().printf("[SignApksBuilder] archiving unsigned APK %s%n", unsignedApk); 368 | apksToArchive.put(archivePrefix + unsignedApk.getName() + "/" + unsignedApk.getName(), relativeToWorkspace(workspace, unsignedApk)); 369 | } 370 | if (getArchiveSignedApks()) { 371 | listener.getLogger().printf("[SignApksBuilder] archiving signed APK %s%n", signedRelName); 372 | apksToArchive.put(archivePrefix + unsignedApk.getName() + "/" + signedApk.getName(), signedRelName); 373 | } 374 | } 375 | 376 | listener.getLogger().println("[SignApksBuilder] finished signing APKs"); 377 | 378 | if (apksToArchive.size() > 0) { 379 | run.pickArtifactManager().archive(workspace, launcher, BuildListenerAdapter.wrap(listener), apksToArchive); 380 | } 381 | } 382 | 383 | private String relativeToWorkspace(FilePath ws, FilePath path) throws IOException, InterruptedException { 384 | URI relUri = ws.toURI().relativize(path.toURI()); 385 | return relUri.getPath().replaceFirst("/$", ""); 386 | } 387 | 388 | private StandardCertificateCredentials getKeystore(String keyStoreName, Item item) { 389 | List creds = CredentialsProvider.lookupCredentials( 390 | StandardCertificateCredentials.class, item, ACL.SYSTEM, NO_REQUIREMENTS); 391 | return CredentialsMatchers.firstOrNull(creds, CredentialsMatchers.withId(keyStoreName)); 392 | } 393 | 394 | @Extension 395 | @Symbol("signAndroidApks") 396 | public static final class SignApksDescriptor extends BuildStepDescriptor { 397 | 398 | static final String DISPLAY_NAME = Messages.builderDisplayName(); 399 | 400 | @Override 401 | public boolean isApplicable(Class jobType) { 402 | return true; 403 | } 404 | 405 | public SignApksDescriptor() { 406 | super(); 407 | load(); 408 | } 409 | 410 | @Override 411 | public @NonNull String getDisplayName() { 412 | return DISPLAY_NAME; 413 | } 414 | 415 | @SuppressWarnings("unused") 416 | public ListBoxModel doFillKeyStoreIdItems(@AncestorInPath ItemGroup parent) { 417 | if (parent == null) { 418 | parent = Jenkins.getInstance(); 419 | } 420 | ListBoxModel items = new ListBoxModel(); 421 | List keys = CredentialsProvider.lookupCredentials( 422 | StandardCertificateCredentials.class, parent, ACL.SYSTEM, SignApksBuilder.NO_REQUIREMENTS); 423 | for (StandardCertificateCredentials key : keys) { 424 | String id = key.getId(); 425 | String label = key.getDescription(); 426 | if (StringUtils.isEmpty(label)) { 427 | label = id; 428 | } 429 | items.add(label, id); 430 | } 431 | return items; 432 | } 433 | 434 | @SuppressWarnings("unused") 435 | public FormValidation doCheckAlias(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { 436 | return FormValidation.validateRequired(value); 437 | } 438 | 439 | @SuppressWarnings("unused") 440 | public FormValidation doCheckApksToSign(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { 441 | if (project == null) { 442 | return FormValidation.warning(Messages.validation_noProject()); 443 | } 444 | project.checkPermission(Item.WORKSPACE); 445 | FilePath someWorkspace = project.getSomeWorkspace(); 446 | if (someWorkspace == null) { 447 | return FormValidation.warning(Messages.validation_noWorkspace()); 448 | } 449 | 450 | String[] globs = getSelectionGlobs(value); 451 | String msg; 452 | for (String glob : globs) { 453 | try { 454 | msg = someWorkspace.validateAntFileMask(value, FilePath.VALIDATE_ANT_FILE_MASK_BOUND); 455 | } 456 | catch (InterruptedException e) { 457 | msg = Messages.validation_globSearchLimitReached(FilePath.VALIDATE_ANT_FILE_MASK_BOUND); 458 | } 459 | if (msg != null) { 460 | return FormValidation.warning(msg); 461 | } 462 | } 463 | return FormValidation.ok(); 464 | } 465 | 466 | } 467 | 468 | static class SignApkCallable extends MasterToSlaveFileCallable { 469 | 470 | private static final long serialVersionUID = 1; 471 | 472 | private final PrivateKey key; 473 | private final Certificate[] certChain; 474 | private final String v1SigName; 475 | private final String outputApk; 476 | private final TaskListener listener; 477 | 478 | SignApkCallable(PrivateKey key, Certificate[] certChain, String v1SigName, String outputApk, TaskListener listener) { 479 | this.key = key; 480 | this.certChain = certChain; 481 | this.v1SigName = v1SigName; 482 | this.outputApk = outputApk; 483 | this.listener = listener; 484 | } 485 | 486 | @Override 487 | public Void invoke(File inputApkFile, VirtualChannel channel) throws IOException, InterruptedException { 488 | 489 | File outputApkFile = new File(outputApk); 490 | if (outputApkFile.isFile()) { 491 | listener.getLogger().printf("[SignApksBuilder] deleting previous signed APK %s%n", outputApk); 492 | if (!outputApkFile.delete()) { 493 | throw new AbortException("failed to delete previous signed APK " + outputApk); 494 | } 495 | } 496 | 497 | List certs = new ArrayList<>(certChain.length); 498 | for (Certificate cert : certChain) { 499 | certs.add((X509Certificate) cert); 500 | } 501 | ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder(v1SigName, key, certs).build(); 502 | List signerConfigs = Collections.singletonList(signerConfig); 503 | 504 | ApkSigner.Builder signerBuilder = new ApkSigner.Builder(signerConfigs) 505 | .setInputApk(inputApkFile) 506 | .setOutputApk(outputApkFile) 507 | .setOtherSignersSignaturesPreserved(false) 508 | // TODO: add to jenkins descriptor 509 | .setV1SigningEnabled(true) 510 | .setV2SigningEnabled(true) 511 | .setV3SigningEnabled(true); 512 | 513 | ApkSigner signer = signerBuilder.build(); 514 | try { 515 | signer.sign(); 516 | } 517 | catch (Exception e) { 518 | PrintWriter details = listener.fatalError("[SignApksBuilder] error signing APK %s", inputApkFile.getAbsolutePath()); 519 | e.printStackTrace(details); 520 | throw new AbortException("failed to sign APK " + inputApkFile.getAbsolutePath() + ": " + e.getLocalizedMessage()); 521 | } 522 | 523 | return null; 524 | } 525 | } 526 | 527 | } 528 | --------------------------------------------------------------------------------