diff --git a/.gitignore b/.gitignore index 79716106..49e65be5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ gradle-app.setting .idea *.iml -local.properties \ No newline at end of file +local.properties diff --git a/.travis.yml b/.travis.yml index 2cf757af..7526cc51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,14 @@ branches: except: - /^v\d/ -#Below skips the installation step completely (https://docs.travis-ci.com/user/customizing-the-build/#Skipping-the-Installation-Step) install: - - true + - export ANDROID_HOME=~/android-sdk-linux + - wget -q "https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip" -O android-sdk-tools.zip + - unzip -q android-sdk-tools.zip -d ${ANDROID_HOME} + - PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools + - yes | sdkmanager --update + - yes | sdkmanager --licenses + - sdkmanager "tools" "ndk-bundle" "build-tools;29.0.0" "platforms;android-29" > /dev/null before_script: - _JAVA_OPTIONS= diff --git a/build.gradle b/build.gradle index 3ee95a87..28f6cbef 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ allprojects { repositories { jcenter() maven { url "https://plugins.gradle.org/m2/" } + maven { url 'https://maven.google.com' } } tasks.withType(Test) { diff --git a/docs/gradle-plugins/android-publish-plugin.md b/docs/gradle-plugins/android-publish-plugin.md new file mode 100644 index 00000000..8261a710 --- /dev/null +++ b/docs/gradle-plugins/android-publish-plugin.md @@ -0,0 +1,27 @@ +### Android libraries support + +Android Gradle Plugin `3.6.0-beta05` or newer is required. + +Configuration specific to Android library projects (using `com.android.library` plugins): + +1. Apply `org.shipkit.android-publish` plugin to each Gradle project (submodule) you want to publish +(usually they are not the root projects). +1. Specify `artifactId` in `androidPublish` blocks. + +Example: + +```Gradle +apply plugin: 'org.shipkit.bintray' +apply plugin: 'org.shipkit.android-publish' +apply plugin: 'com.android.library' + +androidPublish { + artifactId = 'shipkit-android' +} + +``` + +Other POM properties which can be set using Gradle API: +* group id - [Project#group](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:group) +* name - [Project#archivesBaseName](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:archivesBaseName) +* description - [Project#description](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:description) diff --git a/docs/how-shipkit-works.md b/docs/how-shipkit-works.md index e5fdc5e5..9ad36e61 100644 --- a/docs/how-shipkit-works.md +++ b/docs/how-shipkit-works.md @@ -26,6 +26,7 @@ How do we: - [publish binaries](/docs/features/publishing-binaries.md) - [publishing binaries using maven-publish plugin](/docs/features/publishing-binaries-using-maven-publish-plugin.md) - [avoid unnecessary releases](/docs/gradle-plugins/release-needed-plugin.md) +- [support Android libraries](/docs/gradle-plugins/android-publish-plugin.md) - [shipping Javadoc](/docs/features/shipping-javadoc.md) - [automatically include contributors in pom.xml](/docs/features/celebrating-contributors.md) @@ -41,8 +42,8 @@ script: - ./gradlew build -s && ./gradlew ciPerformRelease -s ``` -Those lines means the releasing process is two-stage. -First the `build` Gradle task is executed. +Those lines means the releasing process is two-stage. +First the `build` Gradle task is executed. Shipkit doesn't change there a lot. More interesting is the second task: `ciPerformRelease`. This task depends on 3 another tasks: `releaseNeeded`, `ciReleasePrepare` and `performRelease`. @@ -145,7 +146,7 @@ Text used to create this diagram: https://gist.github.com/mstachniuk/b7cfd3bef9f | | Info is release needed or not | |------------------------------------| | | | |<-------------------------------------------| | | | | | | | - + ``` diff --git a/subprojects/shipkit/src/main/groovy/org/shipkit/gradle/configuration/AndroidPublishConfiguration.java b/subprojects/shipkit/src/main/groovy/org/shipkit/gradle/configuration/AndroidPublishConfiguration.java new file mode 100644 index 00000000..ce72442f --- /dev/null +++ b/subprojects/shipkit/src/main/groovy/org/shipkit/gradle/configuration/AndroidPublishConfiguration.java @@ -0,0 +1,26 @@ +package org.shipkit.gradle.configuration; + +import org.gradle.api.GradleException; + +public class AndroidPublishConfiguration { + + private String artifactId; + + /** + * Artifact id of published AAR + * For example: "shipkit-android" + */ + public String getArtifactId() { + if (artifactId == null || artifactId.isEmpty()) { + throw new GradleException("Please configure artifact id"); + } + return artifactId; + } + + /** + * See {@link #getArtifactId()} ()} + */ + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } +} diff --git a/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/android/AndroidPublishPlugin.java b/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/android/AndroidPublishPlugin.java new file mode 100644 index 00000000..a1c745f9 --- /dev/null +++ b/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/android/AndroidPublishPlugin.java @@ -0,0 +1,82 @@ +package org.shipkit.internal.gradle.android; + +import com.jfrog.bintray.gradle.BintrayExtension; + +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.component.SoftwareComponent; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.publish.maven.MavenPublication; +import org.shipkit.gradle.configuration.AndroidPublishConfiguration; +import org.shipkit.gradle.configuration.ShipkitConfiguration; +import org.shipkit.internal.gradle.configuration.ShipkitConfigurationPlugin; +import org.shipkit.internal.gradle.snapshot.LocalSnapshotPlugin; +import org.shipkit.internal.gradle.util.GradleDSLHelper; +import org.shipkit.internal.gradle.util.PomCustomizer; + +import static org.shipkit.internal.gradle.configuration.DeferredConfiguration.deferredConfiguration; +import static org.shipkit.internal.gradle.java.JavaPublishPlugin.MAVEN_LOCAL_TASK; +import static org.shipkit.internal.gradle.java.JavaPublishPlugin.PUBLICATION_NAME; + +/** + * Publishing Android libraries using 'maven-publish' plugin. + * Intended to be applied in individual Android library submodule. + * Applies following plugins and tasks and configures them: + * + * + * + * Other features: + * + */ +public class AndroidPublishPlugin implements Plugin { + + private final static Logger LOG = Logging.getLogger(AndroidPublishPlugin.class); + private final static String ANDROID_PUBLISH_EXTENSION = "androidPublish"; + + public void apply(final Project project) { + final AndroidPublishConfiguration androidPublishConfiguration = project.getExtensions().create(ANDROID_PUBLISH_EXTENSION, AndroidPublishConfiguration.class); + + final ShipkitConfiguration conf = project.getPlugins().apply(ShipkitConfigurationPlugin.class).getConfiguration(); + + project.getPlugins().apply(LocalSnapshotPlugin.class); + Task snapshotTask = project.getTasks().getByName(LocalSnapshotPlugin.SNAPSHOT_TASK); + snapshotTask.dependsOn(MAVEN_LOCAL_TASK); + + project.getPlugins().apply("maven-publish"); + + BintrayExtension bintray = project.getExtensions().getByType(BintrayExtension.class); + bintray.setPublications(PUBLICATION_NAME); + + project.getPlugins().withId("com.android.library", plugin -> { + deferredConfiguration(project, () -> { + GradleDSLHelper.publications(project, publications -> { + MavenPublication p = publications.create(PUBLICATION_NAME, MavenPublication.class, publication -> { + publication.setArtifactId(androidPublishConfiguration.getArtifactId()); + + SoftwareComponent releaseComponent = project.getComponents().findByName("release"); + if (releaseComponent == null) { + throw new GradleException("'release' component not found in project. " + + "Make sure you are using Android Gradle Plugin 3.6.0-beta05 or newer."); + } + publication.from(releaseComponent); + PomCustomizer.customizePom(project, conf, publication); + }); + LOG.info("{} - configured '{}' publication", project.getPath(), p.getArtifactId()); + }); + }); + + //so that we flesh out problems with maven publication during the build process + project.getTasks().getByName("build").dependsOn(MAVEN_LOCAL_TASK); + }); + } +} diff --git a/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/java/JavaPublishPlugin.java b/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/java/JavaPublishPlugin.java index 3466c193..69206b93 100644 --- a/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/java/JavaPublishPlugin.java +++ b/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/java/JavaPublishPlugin.java @@ -29,7 +29,7 @@ * Other features: * diff --git a/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/util/PomCustomizer.java b/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/util/PomCustomizer.java index 541adfff..a81d3a54 100644 --- a/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/util/PomCustomizer.java +++ b/subprojects/shipkit/src/main/groovy/org/shipkit/internal/gradle/util/PomCustomizer.java @@ -56,7 +56,8 @@ public void execute(XmlProvider xml) { "\n - Contributors read from GitHub: " + StringUtil.join(contributorsFromGitHub.toConfigNotation(), ", ")); - customizePom(xml.asNode(), conf, archivesBaseName, project.getDescription(), contributorsFromGitHub); + final boolean isAndroidLibrary = project.getPlugins().hasPlugin("com.android.library"); + customizePom(xml.asNode(), conf, archivesBaseName, project.getDescription(), contributorsFromGitHub, isAndroidLibrary); } }); } @@ -66,12 +67,14 @@ public void execute(XmlProvider xml) { */ static void customizePom(Node root, ShipkitConfiguration conf, String projectName, String projectDescription, - ProjectContributorsSet contributorsFromGitHub) { - //Assumes project has java plugin applied. Pretty safe assumption + ProjectContributorsSet contributorsFromGitHub, + boolean isAndroidLibrary) { //TODO: we need to conditionally append nodes because given node may already be on the root (issue 847) //TODO: all root.appendNode() need to be conditional root.appendNode("name", projectName); - if (root.getAt(new QName("packaging")).isEmpty()) { + + //Android library publication uses aar packaging + if (!isAndroidLibrary && root.getAt(new QName("packaging")).isEmpty()) { root.appendNode("packaging", "jar"); } diff --git a/subprojects/shipkit/src/main/resources/META-INF/gradle-plugins/org.shipkit.android-publish.properties b/subprojects/shipkit/src/main/resources/META-INF/gradle-plugins/org.shipkit.android-publish.properties new file mode 100644 index 00000000..faf54741 --- /dev/null +++ b/subprojects/shipkit/src/main/resources/META-INF/gradle-plugins/org.shipkit.android-publish.properties @@ -0,0 +1 @@ +implementation-class=org.shipkit.internal.gradle.android.AndroidPublishPlugin diff --git a/subprojects/shipkit/src/test/groovy/org/shipkit/gradle/ShipkitAndroidIntegTest.groovy b/subprojects/shipkit/src/test/groovy/org/shipkit/gradle/ShipkitAndroidIntegTest.groovy new file mode 100644 index 00000000..847ed1a6 --- /dev/null +++ b/subprojects/shipkit/src/test/groovy/org/shipkit/gradle/ShipkitAndroidIntegTest.groovy @@ -0,0 +1,133 @@ +package org.shipkit.gradle + +import org.gradle.testkit.runner.BuildResult +import testutil.GradleSpecification + +class ShipkitAndroidIntegTest extends GradleSpecification { + + void setup() { + settingsFile << "include 'lib'" + newFile('lib/build.gradle') << """ + apply plugin: 'org.shipkit.bintray' + apply plugin: 'org.shipkit.android-publish' + androidPublish { + artifactId = 'shipkit-android' + } + + apply plugin: 'com.android.library' + android { + compileSdkVersion 29 + defaultConfig { + minSdkVersion 29 + } + } + """ + + newFile("gradle/shipkit.gradle") << """ + shipkit { + gitHub.readOnlyAuthToken = "foo" + gitHub.writeAuthToken = "secret" + releaseNotes.file = "CHANGELOG.md" + git.user = "shipkit" + git.email = "shipkit.org@gmail.com" + gitHub.repository = "repo" + } + + allprojects { + plugins.withId("com.jfrog.bintray") { + bintray { + user = "szczepiq" + key = "secret" + } + } + } + """ + buildFile << """ + apply plugin: 'org.shipkit.java' + buildscript { + repositories { + google() + jcenter() + gradlePluginPortal() + } + } + """ + newFile("src/main/AndroidManifest.xml") << """""" + } + + def "all tasks in dry run (gradle #gradleVersionToTest) (AGP #agpVersionToTest)"() { + /** + * TODO this test is just a starting point we will make it better and create more integration tests + * Stuff that we should do: + * 1. (Most important) Avoid writing too many integration tests. Most code should be covered by unit tests + * (see testing pyramid) + * 2. Push out complexity to base class GradleSpecification + * so that what remains in the test is the essential part of a tested feature + * 3. Add more specific assertions rather than just a list of tasks in dry run mode + * 4. Use sensible defaults so that we don't need to specify all configuration in the test + * 5. Move integration tests to a separate module + * 6. Dependencies are hardcoded between GradleSpecification and build.gradle of release-tools project + */ + given: + gradleVersion = gradleVersionToTest + + and: + buildFile << """ + buildscript { + dependencies { + classpath 'com.android.tools.build:gradle:$agpVersionToTest' + } + } + """ + + expect: + BuildResult result = pass("performRelease", "-m", "-s") + //git push and bintray upload tasks should run as late as possible + def output = skippedTaskPathsGradleBugWorkaround(result.output).join("\n") + output.startsWith(""":bumpVersionFile +:identifyGitBranch +:fetchContributors +:fetchReleaseNotes +:updateReleaseNotes +:gitCommit +:gitTag +:gitPush +:performGitPush +:updateReleaseNotesOnGitHub +:lib:preBuild""") + + and: + output.endsWith(""":lib:bintrayUpload +:bintrayPublish +:performRelease""") + + where: + gradleVersionToTest << ["5.6.4", "6.0.1"] + and: + agpVersionToTest << ["3.6.0-beta05", "3.6.0-rc01"] + } + + def "fails on unsupported dependency versions (gradle #gradleVersionToTest) (AGP #agpVersionToTest)"() { + given: + gradleVersion = gradleVersionToTest + + and: + buildFile << """ + buildscript { + dependencies { + classpath 'com.android.tools.build:gradle:$agpVersionToTest' + } + } + """ + + expect: + BuildResult result = fail("performRelease", "-m", "-s") + result.output.contains("'release' component not found in project. " + + "Make sure you are using Android Gradle Plugin 3.6.0-beta05 or newer.") + + where: + gradleVersionToTest << ["5.6.4", "6.0.1"] + and: + agpVersionToTest << ["3.4.0", "3.5.2"] + } +} diff --git a/subprojects/shipkit/src/test/groovy/org/shipkit/gradle/configuration/AndroidPublishConfigurationTest.groovy b/subprojects/shipkit/src/test/groovy/org/shipkit/gradle/configuration/AndroidPublishConfigurationTest.groovy new file mode 100644 index 00000000..51b468e6 --- /dev/null +++ b/subprojects/shipkit/src/test/groovy/org/shipkit/gradle/configuration/AndroidPublishConfigurationTest.groovy @@ -0,0 +1,25 @@ +package org.shipkit.gradle.configuration + +import org.gradle.api.GradleException +import spock.lang.Specification + +class AndroidPublishConfigurationTest extends Specification { + + def conf = new AndroidPublishConfiguration(); + + def "throws when artifact id not configured"() { + when: + conf.artifactId + + then: + thrown(GradleException) + } + + def "stores artifact id"() { + when: + conf.artifactId = "org.shipkit.android" + + then: + conf.artifactId == "org.shipkit.android" + } +} diff --git a/subprojects/shipkit/src/test/groovy/org/shipkit/internal/gradle/util/PomCustomizerTest.groovy b/subprojects/shipkit/src/test/groovy/org/shipkit/internal/gradle/util/PomCustomizerTest.groovy index bfcca437..45794d65 100644 --- a/subprojects/shipkit/src/test/groovy/org/shipkit/internal/gradle/util/PomCustomizerTest.groovy +++ b/subprojects/shipkit/src/test/groovy/org/shipkit/internal/gradle/util/PomCustomizerTest.groovy @@ -35,7 +35,7 @@ class PomCustomizerTest extends Specification { //wwilk will not be duplicated in developers/contributors conf.team.contributors = ["mstachniuk:Marcin Stachniuk", "wwilk:Wojtek Wilk"] - PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet()) + PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet(), false) expect: printXml(node) == """ @@ -98,7 +98,7 @@ class PomCustomizerTest extends Specification { contributorsSet.addContributor(new DefaultProjectContributor("Wojtek Wilk", "wwilk", "https://github.com/wwilk", 5)) contributorsSet.addContributor(new DefaultProjectContributor("Marcin Stachniuk", "mstachniuk", "https://github.com/mstachniuk", 3)) - PomCustomizer.customizePom(node, conf, "foo", "Foo library", contributorsSet) + PomCustomizer.customizePom(node, conf, "foo", "Foo library", contributorsSet, false) expect: printXml(node) == """ @@ -157,7 +157,7 @@ class PomCustomizerTest extends Specification { conf.team.developers = [] conf.team.contributors = [] - PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet()) + PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet(), false) expect: printXml(node) == """ @@ -187,12 +187,46 @@ class PomCustomizerTest extends Specification { """ } + def "AAR packaging in Android library"() { + conf.gitHub.repository = "repo" + node.appendNode("packaging", "aar") + + PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet(), true) + + expect: + printXml(node) == """ + aar + foo + https://github.com/repo + Foo library + + + The MIT License + https://github.com/repo/blob/master/LICENSE + repo + + + + https://github.com/repo.git + + + https://github.com/repo/issues + GitHub issues + + + https://travis-ci.org/repo + TravisCI + + +""" + } + def "CI management from configuration"() { conf.gitHub.repository = "repo" conf.ciManagement.system = "Bitrise" conf.ciManagement.url = "https://app.bitrise.io/app/slug" - PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet()) + PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet(), false) expect: printXml(node) == """ @@ -225,7 +259,7 @@ class PomCustomizerTest extends Specification { def "default CI management"() { conf.gitHub.repository = "repo" - PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet()) + PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet(), false) expect: printXml(node) == """ @@ -260,7 +294,7 @@ class PomCustomizerTest extends Specification { conf.gitHub.repository = "repo" node.appendNode("packaging", "unbundled"); - PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet()) + PomCustomizer.customizePom(node, conf, "foo", "Foo library", new DefaultProjectContributorsSet(), false) expect: printXml(node) == """ diff --git a/version.properties b/version.properties index 98011ba7..fa07a47e 100644 --- a/version.properties +++ b/version.properties @@ -1,6 +1,6 @@ #Version of the produced binaries. This file is intended to be checked-in. #It will be automatically bumped by release automation. -version=2.2.8 +version=2.3.0 #Last previous release version previousVersion=2.2.7