From b4d66af1daea5505fc56fb6988af9679643eddd5 Mon Sep 17 00:00:00 2001 From: Simon Wimmesberger Date: Sun, 27 Sep 2020 19:10:21 +0200 Subject: [PATCH] Publish OSGi metadata for OkHttp --- CONTRIBUTING.md | 1 + build.gradle | 32 +++- okhttp-brotli/build.gradle | 11 +- okhttp-dnsoverhttps/build.gradle | 11 +- okhttp-logging-interceptor/build.gradle | 10 +- okhttp-sse/build.gradle | 10 +- okhttp-tls/build.gradle | 10 +- okhttp-urlconnection/build.gradle | 15 +- okhttp/build.gradle | 41 ++++- .../src/test/java/okhttp3/osgi/OsgiTest.java | 159 ++++++++++++++++++ 10 files changed, 274 insertions(+), 26 deletions(-) create mode 100644 okhttp/src/test/java/okhttp3/osgi/OsgiTest.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 973d385648ac..bed49e2e904d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,3 +59,4 @@ Committer's Guides [releasing]: http://square.github.io/okhttp/releasing/ [security]: http://square.github.io/okhttp/security/ [works_with_okhttp]: http://square.github.io/okhttp/works_with_okhttp/ + [okhttp_build]: https://github.com/square/okhttp/blob/master/okhttp/build.gradle diff --git a/build.gradle b/build.gradle index 711ef0978ff0..6e3fb2011c75 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -import net.ltgt.gradle.errorprone.CheckSeverity - buildscript { ext.versions = [ 'animalSniffer': '1.19', @@ -20,7 +18,9 @@ buildscript { 'okio': '2.9.0', 'ktlint': '0.38.0', 'picocli': '4.5.1', - 'openjsse': '1.1.0' + 'openjsse': '1.1.0', + 'bnd': '5.1.2', + 'equinox': '3.16.0' ] ext.deps = [ @@ -43,13 +43,18 @@ buildscript { 'moshi': "com.squareup.moshi:moshi:${versions.moshi}", 'moshiKotlin': "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}", 'okio': "com.squareup.okio:okio:${versions.okio}", - 'openjsse': "org.openjsse:openjsse:${versions.openjsse}" + 'openjsse': "org.openjsse:openjsse:${versions.openjsse}", + 'bnd': "biz.aQute.bnd:biz.aQute.bnd.gradle:${versions.bnd}", + 'bndResolve': "biz.aQute.bnd:biz.aQute.resolve:${versions.bnd}", + 'equinox': "org.eclipse.platform:org.eclipse.osgi:${versions.equinox}", + 'kotlinStdlibOsgi': "org.jetbrains.kotlin:kotlin-osgi-bundle:${versions.kotlin}" ] dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10" classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.10.1" classpath "com.android.tools.build:gradle:4.0.1" + classpath deps.bnd } repositories { @@ -107,6 +112,23 @@ allprojects { } } +ext.applyOsgi = { project -> + project.apply plugin: 'biz.aQute.bnd.builder' + + project.sourceSets { + osgi + } + + project.jar { t -> + t.setClasspath(project.sourceSets.osgi['compileClasspath'] + project.sourceSets.main['compileClasspath']) + } + + project.dependencies { + // The OSGi kotlin-stdlib lets bnd infer bundle versions. + osgiApi deps.kotlinStdlibOsgi + } +} + /** Configure building for Java+Kotlin projects. */ subprojects { project -> if (project.name == 'android-test') return @@ -118,6 +140,8 @@ subprojects { project -> apply plugin: 'checkstyle' apply plugin: 'ru.vyarus.animalsniffer' apply plugin: 'org.jetbrains.dokka' + apply plugin: 'biz.aQute.bnd.builder' + sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/okhttp-brotli/build.gradle b/okhttp-brotli/build.gradle index 2199662f9099..a9a8d488ecbc 100644 --- a/okhttp-brotli/build.gradle +++ b/okhttp-brotli/build.gradle @@ -1,7 +1,12 @@ +applyOsgi(this) + jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3.brotli') - } + // MANIFEST.MF, including OSGi bnd instructions. + bnd ''' + Export-Package: okhttp3.brotli + Automatic-Module-Name: okhttp3.brotli + Bundle-SymbolicName: com.squareup.okhttp3.brotli + ''' } dependencies { diff --git a/okhttp-dnsoverhttps/build.gradle b/okhttp-dnsoverhttps/build.gradle index 87479d7cf5af..d1c567e01f5e 100644 --- a/okhttp-dnsoverhttps/build.gradle +++ b/okhttp-dnsoverhttps/build.gradle @@ -1,7 +1,12 @@ +applyOsgi(this) + jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3.dnsoverhttps') - } + // MANIFEST.MF, including OSGi bnd instructions. + bnd ''' + Export-Package: okhttp3.dnsoverhttps + Automatic-Module-Name: okhttp3.dnsoverhttps + Bundle-SymbolicName: com.squareup.okhttp3.dnsoverhttps + ''' } dependencies { diff --git a/okhttp-logging-interceptor/build.gradle b/okhttp-logging-interceptor/build.gradle index fae4fd07fb2a..194b7a74a34a 100644 --- a/okhttp-logging-interceptor/build.gradle +++ b/okhttp-logging-interceptor/build.gradle @@ -1,9 +1,13 @@ apply plugin: 'me.champeau.gradle.japicmp' +applyOsgi(this) jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3.logging') - } + // MANIFEST.MF, including OSGi bnd instructions. + bnd ''' + Export-Package: okhttp3.logging + Automatic-Module-Name: okhttp3.logging + Bundle-SymbolicName: com.squareup.okhttp3.logging + ''' } dependencies { diff --git a/okhttp-sse/build.gradle b/okhttp-sse/build.gradle index 6b0916d8aa81..d283f7bbabcf 100644 --- a/okhttp-sse/build.gradle +++ b/okhttp-sse/build.gradle @@ -1,9 +1,13 @@ apply plugin: 'me.champeau.gradle.japicmp' +applyOsgi(this) jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3.sse') - } + // MANIFEST.MF, including OSGi bnd instructions. + bnd ''' + Export-Package: okhttp3.sse + Automatic-Module-Name: okhttp3.sse + Bundle-SymbolicName: com.squareup.okhttp3.sse + ''' } dependencies { diff --git a/okhttp-tls/build.gradle b/okhttp-tls/build.gradle index 47dbf62ea1d6..87561dce665c 100644 --- a/okhttp-tls/build.gradle +++ b/okhttp-tls/build.gradle @@ -1,9 +1,13 @@ apply plugin: 'me.champeau.gradle.japicmp' +applyOsgi(this) jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3.tls') - } + // MANIFEST.MF, including OSGi bnd instructions. + bnd ''' + Export-Package: okhttp3.tls + Automatic-Module-Name: okhttp3.tls + Bundle-SymbolicName: com.squareup.okhttp3.tls + ''' } dependencies { diff --git a/okhttp-urlconnection/build.gradle b/okhttp-urlconnection/build.gradle index 16eeb1b70f17..e95dbd232769 100644 --- a/okhttp-urlconnection/build.gradle +++ b/okhttp-urlconnection/build.gradle @@ -1,13 +1,20 @@ apply plugin: 'me.champeau.gradle.japicmp' +applyOsgi(this) +def mainProj = project(':okhttp') jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3.urlconnection') - } + // MANIFEST.MF, including OSGi bnd instructions. + // urlconnection needs to be an OSGi fragment because its package name is the same as okhttp3's. + bnd """ + Fragment-Host: com.squareup.okhttp3; bundle-version="\${range;[==,+);\${version_cleanup;${mainProj.version}}}" + Automatic-Module-Name: okhttp3.urlconnection + Bundle-SymbolicName: com.squareup.okhttp3.urlconnection + -removeheaders: Private-Package + """ } dependencies { - api project(':okhttp') + api mainProj compileOnly deps.jsr305 compileOnly deps.animalSniffer diff --git a/okhttp/build.gradle b/okhttp/build.gradle index b4416288b6d8..58319e0827dc 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -1,9 +1,26 @@ apply plugin: 'me.champeau.gradle.japicmp' +applyOsgi(this) jar { - manifest { - attributes('Automatic-Module-Name': 'okhttp3') - } + // MANIFEST.MF, including OSGi bnd instructions. + // We export okhttp3.internal for our own modules use. + // The packages of all optional dependencies must be explicitly specified. + bnd ''' + Export-Package: \ + okhttp3,\ + okhttp3.internal.*;okhttpinternal=true;mandatory:=okhttpinternal + Import-Package: \ + android.*;resolution:=optional,\ + com.oracle.svm.core.annotate;resolution:=optional,\ + dalvik.system;resolution:=optional,\ + org.conscrypt;resolution:=optional,\ + org.bouncycastle.*;resolution:=optional,\ + org.openjsse.*;resolution:=optional,\ + sun.security.ssl;resolution:=optional,\ + * + Automatic-Module-Name: okhttp3 + Bundle-SymbolicName: com.squareup.okhttp3 + ''' } sourceSets { @@ -21,9 +38,21 @@ task copyJavaTemplates(type: Copy) { filteringCharset = 'UTF-8' } +// Expose OSGi jars to the test environment. +configurations { + osgiTestDeploy +} +task copyOsgiTestDeployment(type: Copy) { + from configurations.osgiTestDeploy + into "${buildDir}/resources/test/okhttp3/osgi/deployments" +} +tasks.test.dependsOn(copyOsgiTestDeployment) + dependencies { api deps.okio api deps.kotlinStdlib + + // These compileOnly dependencies must also be listed in the OSGi configuration above. compileOnly deps.android compileOnly deps.bouncycastle compileOnly deps.bouncycastletls @@ -47,10 +76,16 @@ dependencies { testImplementation project(':okhttp-urlconnection') testImplementation project(':mockwebserver') testImplementation project(':okhttp-logging-interceptor') + testImplementation project(':okhttp-brotli') + testImplementation project(':okhttp-dnsoverhttps') + testImplementation project(':okhttp-sse') testImplementation deps.conscrypt testImplementation deps.junit testImplementation deps.assertj testImplementation deps.openjsse + testImplementation deps.bndResolve + osgiTestDeploy deps.equinox + osgiTestDeploy deps.kotlinStdlibOsgi testCompileOnly deps.jsr305 } diff --git a/okhttp/src/test/java/okhttp3/osgi/OsgiTest.java b/okhttp/src/test/java/okhttp3/osgi/OsgiTest.java new file mode 100644 index 000000000000..78da5487e058 --- /dev/null +++ b/okhttp/src/test/java/okhttp3/osgi/OsgiTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.osgi; + +import aQute.bnd.build.Project; +import aQute.bnd.build.Workspace; +import aQute.bnd.build.model.BndEditModel; +import aQute.bnd.deployer.repository.LocalIndexedRepo; +import aQute.bnd.osgi.Constants; +import aQute.bnd.service.RepositoryPlugin; +import biz.aQute.resolve.Bndrun; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import okio.BufferedSource; +import okio.Okio; +import org.junit.Before; +import org.junit.Test; + +public final class OsgiTest { + /** Each is the Bundle-SymbolicName of an OkHttp module's OSGi configuration. */ + private static final List REQUIRED_BUNDLES = Arrays.asList( + "com.squareup.okhttp3", + "com.squareup.okhttp3.brotli", + "com.squareup.okhttp3.dnsoverhttps", + "com.squareup.okhttp3.logging", + "com.squareup.okhttp3.sse", + "com.squareup.okhttp3.tls", + "com.squareup.okhttp3.urlconnection" + ); + + /** Equinox must also be on the testing classpath. */ + private static final String RESOLVE_OSGI_FRAMEWORK = "org.eclipse.osgi"; + private static final String RESOLVE_JAVA_VERSION = "JavaSE-1.8"; + private static final String REPO_NAME = "OsgiTest"; + + private File testResourceDir; + private File workspaceDir; + + @Before + public void setUp() throws Exception { + testResourceDir = new File("./build/resources/test/okhttp3/osgi"); + workspaceDir = new File(testResourceDir, "workspace"); + + // Ensure we start from scratch. + deleteDirectory(workspaceDir); + workspaceDir.mkdirs(); + } + + /** + * Resolve the OSGi metadata of the all okhttp3 modules. If required modules do not have OSGi + * metadata this will fail with an exception. + */ + @Test + public void testMainModuleWithSiblings() throws Exception { + try (Workspace workspace = createWorkspace(); + Bndrun bndRun = createBndRun(workspace)) { + bndRun.resolve(false, false); + } + } + + private Workspace createWorkspace() throws Exception { + File bndDir = new File(workspaceDir, "cnf"); + File repoDir = new File(bndDir, "repo"); + repoDir.mkdirs(); + + Workspace workspace = new Workspace(workspaceDir, bndDir.getName()); + workspace.setProperty(Constants.PLUGIN + "." + REPO_NAME, "" + + LocalIndexedRepo.class.getName() + + "; " + LocalIndexedRepo.PROP_NAME + " = '" + REPO_NAME + "'" + + "; " + LocalIndexedRepo.PROP_LOCAL_DIR + " = '" + repoDir + "'"); + workspace.refresh(); + prepareWorkspace(workspace); + return workspace; + } + + private void prepareWorkspace(Workspace workspace) throws Exception { + RepositoryPlugin repositoryPlugin = workspace.getRepository(REPO_NAME); + + // Deploy the bundles in the deployments test directory. + deployDirectory(repositoryPlugin, new File(testResourceDir, "deployments")); + deployClassPath(repositoryPlugin); + } + + private Bndrun createBndRun(Workspace workspace) throws Exception { + // Creating the run require string. It will always use the latest version of each bundle + // available in the repository. + String runRequireString = REQUIRED_BUNDLES.stream() + .map(s -> "osgi.identity;filter:='(osgi.identity=" + s + ")'") + .collect(Collectors.joining(",")); + + BndEditModel bndEditModel = new BndEditModel(workspace); + // Temporary project to satisfy bnd API. + bndEditModel.setProject(new Project(workspace, workspaceDir)); + + Bndrun result = new Bndrun(bndEditModel); + result.setRunfw(RESOLVE_OSGI_FRAMEWORK); + result.setRunee(RESOLVE_JAVA_VERSION); + result.setRunRequires(runRequireString); + return result; + } + + private void deployDirectory(RepositoryPlugin repository, File directory) throws Exception { + File[] files = directory.listFiles(); + if (files == null) return; + + for (File file : files) { + deployFile(repository, file); + } + } + + private void deployClassPath(RepositoryPlugin repositoryPlugin) throws Exception { + String classpath = System.getProperty("java.class.path"); + for (String classPathEntry : classpath.split(File.pathSeparator)) { + deployFile(repositoryPlugin, new File(classPathEntry)); + } + } + + private void deployFile(RepositoryPlugin repositoryPlugin, File file) throws Exception { + if (!file.exists() || file.isDirectory()) return; + + try (BufferedSource source = Okio.buffer(Okio.source(file))) { + repositoryPlugin.put(source.inputStream(), new RepositoryPlugin.PutOptions()); + System.out.println("Deployed " + file.getName()); + } catch (IllegalArgumentException e) { + if (e.getMessage().contains("Jar does not have a symbolic name")) { + System.out.println("Skipped non-OSGi dependency: " + file.getName()); + return; + } + throw e; + } + } + + private static void deleteDirectory(File dir) throws IOException { + if (!dir.exists()) return; + + Files.walk(dir.toPath()) + .filter(Files::isRegularFile) + .map(Path::toFile) + .forEach(File::delete); + } +}