├── 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 | 
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 | 
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 extends AbstractProject> 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 |
--------------------------------------------------------------------------------