Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added OSGi metadata via build #6282

Closed
wants to merge 10 commits into from
35 changes: 31 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import net.ltgt.gradle.errorprone.CheckSeverity

buildscript {
ext.versions = [
'animalSniffer': '1.18',
Expand All @@ -20,7 +18,9 @@ buildscript {
'okio': '2.8.0',
'ktlint': '0.38.0',
'picocli': '4.2.0',
'openjsse': '1.1.0'
'openjsse': '1.1.0',
'bnd': '5.1.2',
'equinox': '3.16.0'
]

ext.deps = [
Expand All @@ -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}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a 4.2M .jar, compared to 1.4M for kotlin-stdlib, presumably because this includes kotlin-reflect.

]

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 {
Expand Down Expand Up @@ -107,6 +112,26 @@ allprojects {
}
}

ext.applyOsgi = { project ->
project.apply plugin: 'biz.aQute.bnd.builder'

project.sourceSets {
// own source set for OSGi specific dependencies
osgi
}

project.jar { t ->
// this sets only the source set for OSGi
// setSourceSet is a method provided by bnd
t.setClasspath(project.sourceSets.osgi['compileClasspath'] + project.sourceSets.main['compileClasspath'])
}

project.dependencies {
// when we provide the OSGi version for the kotlin standard library BND can infer the bundle version for the import package statements
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tricky. Presumably this doesn’t show up elsewhere. Whew.

osgiApi deps.kotlinStdlibOsgi
}
}

/** Configure building for Java+Kotlin projects. */
subprojects { project ->
if (project.name == 'android-test') return
Expand All @@ -118,6 +143,8 @@ subprojects { project ->
apply plugin: 'checkstyle'
apply plugin: 'ru.vyarus.animalsniffer'
apply plugin: 'org.jetbrains.dokka'


sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

Expand Down
11 changes: 8 additions & 3 deletions okhttp-brotli/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
applyOsgi(this)

jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3.brotli')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
bnd '''
Export-Package: okhttp3.brotli
Automatic-Module-Name: okhttp3.brotli
Bundle-SymbolicName: com.squareup.okhttp3.brotli
'''
}

dependencies {
Expand Down
11 changes: 8 additions & 3 deletions okhttp-dnsoverhttps/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
applyOsgi(this)

jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3.dnsoverhttps')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
bnd '''
Export-Package: okhttp3.dnsoverhttps
Automatic-Module-Name: okhttp3.dnsoverhttps
Bundle-SymbolicName: com.squareup.okhttp3.dnsoverhttps
'''
}

dependencies {
Expand Down
10 changes: 7 additions & 3 deletions okhttp-logging-interceptor/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
apply plugin: 'me.champeau.gradle.japicmp'
applyOsgi(this)

jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3.logging')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
bnd '''
Export-Package: okhttp3.logging
Automatic-Module-Name: okhttp3.logging
Bundle-SymbolicName: com.squareup.okhttp3.logging
'''
}

dependencies {
Expand Down
11 changes: 8 additions & 3 deletions okhttp-sse/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
apply plugin: 'me.champeau.gradle.japicmp'
applyOsgi(this)

jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3.sse')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
// is okhttp3.internal.sse package used in dependent libraries?
bnd '''
Export-Package: okhttp3.sse
Automatic-Module-Name: okhttp3.sse
Bundle-SymbolicName: com.squareup.okhttp3.sse
'''
}

dependencies {
Expand Down
10 changes: 7 additions & 3 deletions okhttp-tls/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
apply plugin: 'me.champeau.gradle.japicmp'
applyOsgi(this)

jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3.tls')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
bnd '''
Export-Package: okhttp3.tls
Automatic-Module-Name: okhttp3.tls
Bundle-SymbolicName: com.squareup.okhttp3.tls
'''
}

dependencies {
Expand Down
15 changes: 11 additions & 4 deletions okhttp-urlconnection/build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
apply plugin: 'me.champeau.gradle.japicmp'
applyOsgi(this)

def mainProj = project(':okhttp')
jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3.urlconnection')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
// urlconnection is a OSGi fragment because the package name is the same as the okhttp3 module
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

Expand Down
38 changes: 35 additions & 3 deletions okhttp/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
apply plugin: 'me.champeau.gradle.japicmp'
applyOsgi(this)

jar {
manifest {
attributes('Automatic-Module-Name': 'okhttp3')
}
// modify these lines for MANIFEST.MF properties or for specific bnd instructions
// internal package needs to be exported too because sibling libraries use internal packages
// if a dependency is added which is not necessarily required at runtime add the packages to the optional list
bnd '''
Export-Package: \
okhttp3,\
okhttp3.internal.*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a concept of a friend package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See okhttp-tls comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in zipkin we do things like this..

export your internal package..
https://github.com/openzipkin/brave/blob/master/brave/bnd.bnd#L9

explicitly import when internal is used
https://github.com/openzipkin/brave/blob/master/context/slf4j/bnd.bnd#L1

block import when something is source retention..
https://github.com/openzipkin/brave/blob/master/instrumentation/http/bnd.bnd#L2

Copy link
Contributor Author

@swimmesberger swimmesberger Sep 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure I understand the approach you set the "braveinternal" attribute mandatory* which enforces that the "braveinternal" attribute is set for every Import-Package statement? Therefore if someone wants to Import one of the internal packages the "braveinternal" attribute must explicitly set otherwise the bundle would not resolve?

So for my understanding this would be like a "I agree that what I'm importing is not public API and can change at will". I kind of like that approach because it prevents accidently importing a package which you shouldn't because it's internal.

* For Reference: https://docs.osgi.org/specification/osgi.core/7.0.0/framework.module.html#i2515263

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you understand it, yes. We have had bugs where we forgot to explicitly import the internal thing, but I think it is a better optimization anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did hope that bnd does not specify mandatory attributes automatically but I have just tried it and bnd does in fact automatically infer mandatory attributes for Import-Package statements. Therefore if someone is using bnd as a build system he wouldn't even notice that there is an mandatory attribute called "okhttpinternal"/"braveinternal"...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trying to understand the impact of this.. third parties outside this repo shouldn't be using the internal code anyway, so should be not impacted by ignorance of them. Only here, we have to make sure the bnds are coherent.. am I missing something?

Copy link
Contributor Author

@swimmesberger swimmesberger Sep 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to ensure that only the okhttp sibling libraries are able to use the internal package of the main okhttp project. Therefore we add a mandatory "okhttpinternal" directive which ensures that for each Import-Package: okhttp3.internal statement the attribute must be explicitly defined. For us it's easy to define that attribute for the siblings okhttp3 libraries which use those internal packages. If a 3rdparty consumer would also import one of the internal packages without defining the attribute the bundle would resolve with an error according to OSGi spec because the attribute is not defined.
Unfortunately if the 3rdparty is also using bnd as a build system (like we in our internal project for example) we could freely use the okhttp3.internal package (even though we shouldn't) and we wouldn't event notice that we should define a mandatory attribute because bnd figures that out automatically.
As an example: I tried to add that attribute to the root okhttp3 project and I didn't have to specify the attribute in the sibling okhttp-sse library manually because bnd automatically inferred the mandatory attribute which kind of defeats the whole purpose of the attribute.

Manifest-Version: 1.0
Automatic-Module-Name: okhttp3.sse
Bnd-LastModified: 1601454505191
Bundle-ManifestVersion: 2
Bundle-Name: com.squareup.okhttp3.sse
Bundle-SymbolicName: com.squareup.okhttp3.sse
Bundle-Version: 4.10.0.SNAPSHOT
Created-By: 14.0.2 (AdoptOpenJDK)
Export-Package: okhttp3.sse;uses:="kotlin,kotlin.jvm,okhttp3";version=
 "4.10.0"
Import-Package: kotlin;version="[1.4,2)",kotlin.io;version="[1.4,2)",k
 otlin.jvm;version="[1.4,2)",kotlin.jvm.internal;version="[1.4,2)",okh
 ttp3;version="[4.10,5)",okhttp3.internal;version="[4.10,5)";okhttpint
 ernal=true,okhttp3.internal.connection;version="[4.10,5)";okhttpinter
 nal=true,okio
Private-Package: okhttp3.internal.sse
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
Tool: Bnd-5.1.2.202007211702

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this automatically sucks in the internal stuff, it would seem to be a behavioral difference in manifest generation here vs for example maven-bundle-plugin. This might be a different interpretation of what is sane behavior, or simply an accident in one or the other.

In any case, would you agree that the internal attribution is important to annotate even the gradle plugin or default configuration thwarts that guard rail?

cc also @helgoboss for input as it is sometimes hard for project teams to come to a sane approach for things like this if they aren't actively using the tooling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the attribute although when bnd is used the benefit is negligible. I did not have to explicitly define the import in the sibling libraries because bnd adds the imports with the property set to true automatically...

Import-Package: \
android.*;resolution:=optional,\
dalvik.system;resolution:=optional,\
org.conscrypt;resolution:=optional,\
org.bouncycastle.*;resolution:=optional,\
org.openjsse.*;resolution:=optional,\
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we introduce new dependencies, is there a test that’ll fail to remind us to update this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If new dependencies are introduced you have to decide on a case by case basis if it's an optional dependency or not. If you forget to add the library in the list the worst case will be that a OSGi user of that library has to put that library on the classpath to ensure OSGi can resolve all imports. The resolution optional attribute basically means that those packages are not required at runtime.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a move the comment about optional (ex why not Import-Package: *) here?

Platform or similar might need an update to mention when adding optional library support, to not forget to update this and wherever the test cases are! It could also be useful to use those paths in the OSGi test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add a simple "*" here all the android/ssl packages would be required on the classpath for the bundle to resolve therefore I explicitly define the packages which are not required in every case at runtime.

Hmm... maybe we should add the comment to the build.gradle dependency block instead of the platform class because the dependency block must be touched to add new dependencies. I kind of dislike adding comments to code which are build related.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I meant whatever the package-info equiv of kotlin. I suspect any new member of this directory would possibly taint here. https://github.com/square/okhttp/tree/master/okhttp/src/main/kotlin/okhttp3/internal/platform

Not too fussed where the comment goes, it can be in the CONTRIBUTING file.

thx for the clarification on not '*' that part could be a comment inline here in the gradle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added documentation to the CONTRIBUTING file

sun.security.ssl;resolution:=optional,\
*
Automatic-Module-Name: okhttp3
Bundle-SymbolicName: com.squareup.okhttp3
'''
}

sourceSets {
Expand All @@ -21,6 +37,16 @@ task copyJavaTemplates(type: Copy) {
filteringCharset = 'UTF-8'
}

// make osgiTestImplementation configuration jars available to the test environment
configurations {
osgiTestImplementation
}
task copyOsgiTestDeployment(type: Copy) {
from configurations.osgiTestImplementation
into "${buildDir}/resources/test/okhttp3/osgi/deployments"
}
tasks.test.dependsOn(copyOsgiTestDeployment)

dependencies {
api deps.okio
api deps.kotlinStdlib
Expand All @@ -37,10 +63,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
osgiTestImplementation deps.equinox
osgiTestImplementation deps.kotlinStdlibOsgi
testCompileOnly deps.jsr305
}

Expand Down
96 changes: 96 additions & 0 deletions okhttp/src/test/java/okhttp3/osgi/OkHttpOsgiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package okhttp3.osgi;

import aQute.bnd.build.Workspace;
import aQute.bnd.service.RepositoryPlugin;
import aQute.lib.io.IO;
import biz.aQute.resolve.Bndrun;
import org.junit.Test;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

/**
* Tries to resolve the OSGi metadata specified in the

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someone died writing? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah - fixed it 🤷‍♂️

*/
public class OkHttpOsgiTest {
@Test
public void testMainModuleWithSiblings() throws Exception {
File workspaceDir = getWorkspaceDirectory().toFile();
Path resourceDir = getResourceDirectory();
Workspace workspace = new Workspace(workspaceDir);

RepositoryPlugin localRepo = workspace.getRepository("Local");
// deploy the bundles in the deployments test directory
deployDirectory(localRepo, resourceDir.resolve("deployments"));
deployClassPath(localRepo);

try (Bndrun bndRun = new Bndrun(workspace, resourceDir.resolve("resolveTest.bndrun").toFile())) {
// this will fail when something is wrong
bndRun.resolve(false, true);
}
}

private Path getResourceDirectory() throws URISyntaxException {
return getWorkspaceDirectory().getParent();
}

private Path getWorkspaceDirectory() throws URISyntaxException {
URL bndWorkspaceURL = OkHttpOsgiTest.class.getResource("ws1/cnf/build.bnd");
Path bndWorkspaceFile = Paths.get(bndWorkspaceURL.toURI());
return bndWorkspaceFile.getParent().getParent();
}

private void deployDirectory(RepositoryPlugin repository, Path directory) throws IOException {
if (!Files.exists(directory))
return;

try (Stream<Path> files = Files.list(directory)) {
files.forEach(f -> {
deployFile(repository, f);
});
}
}

private void deployClassPath(RepositoryPlugin repository) {
String classpath = System.getProperty("java.class.path");
String[] classpathEntries = classpath.split(File.pathSeparator);
for (String classPathEntry : classpathEntries) {
Path classPathFile = Paths.get(classPathEntry);
deployFile(repository, classPathFile);
}
}

private void deployFile(RepositoryPlugin repository, Path file) {
if (Files.isRegularFile(file)) {
String fileName = file.getFileName().toString();
try {
deploy(repository, file);
successDeployment(fileName);
} catch (Exception e) {
failedDeployment(fileName, e);
}
}
}

private void deploy(RepositoryPlugin repository, Path bundleFile) throws Exception {
try (InputStream stream = new BufferedInputStream(IO.stream(bundleFile))) {
repository.put(stream, new RepositoryPlugin.PutOptions());
}
}

private void successDeployment(String fileName) {
System.out.println("Deployed " + fileName);
}

private void failedDeployment(String fileName, Exception error) {
System.out.println("Failed to deploy " + fileName);
}
}
13 changes: 13 additions & 0 deletions okhttp/src/test/resources/okhttp3/osgi/resolveTest.bndrun
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-runfw: org.eclipse.osgi

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels weird to split responsibility between code and text like this. I'm not sure it is bad per se, but I think sometimes of testcontainers and enjoy seeing the setup logic in one markup (even if that is compiled :) )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved the bnd instructions from the bndrun, cnf/build.bnd into code so everything should be in one place now.

-runee: JavaSE-1.8
-runsystemcapabilities: ${native_capability}
-resolve.effective: active;skip:="osgi.service"

-runrequires: \
bnd.identity;version='latest';id='com.squareup.okhttp3',\
bnd.identity;version='latest';id='com.squareup.okhttp3.brotli',\
bnd.identity;version='latest';id='com.squareup.okhttp3.dnsoverhttps',\
bnd.identity;version='latest';id='com.squareup.okhttp3.logging',\
bnd.identity;version='latest';id='com.squareup.okhttp3.sse',\
bnd.identity;version='latest';id='com.squareup.okhttp3.tls',\
bnd.identity;version='latest';id='com.squareup.okhttp3.urlconnection'
5 changes: 5 additions & 0 deletions okhttp/src/test/resources/okhttp3/osgi/ws1/cnf/build.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-plugin.1.Local: \
aQute.bnd.deployer.repository.LocalIndexedRepo; \
name = Local; \
pretty = true; \
local = ${build}/local
Empty file.