Skip to content

Commit f23475c

Browse files
authored
Added support for Revapi (com-lihaoyi#3974)
Added [Revapi](https://revapi.org/revapi-site/main/index.html) support for API analysis and change tracking to identify incompatibilities. Resolves com-lihaoyi#3929.
1 parent 7457601 commit f23475c

File tree

20 files changed

+748
-0
lines changed

20 files changed

+748
-0
lines changed

docs/modules/ROOT/pages/javalib/publishing.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,29 @@ This page will discuss common topics around publishing your Java projects for ot
99

1010
include::partial$example/javalib/publishing/2-publish-module.adoc[]
1111

12+
== Checking API compatibility
13+
14+
Mill provides the ability to check API changes with the https://revapi.org/revapi-site/main/index.html[Revapi] analysis and change tracking tool.
15+
16+
include::partial$example/javalib/publishing/3-revapi.adoc[]
17+
18+
CAUTION: The `revapi` task does not fail if incompatibilities are reported. You should fix these, and verify by re-running `revapi`, before a release.
19+
20+
[TIP]
21+
====
22+
The `revapi` task returns the path to a directory that can be used to resolve the relative path to any extension configuration output.
23+
[source,json]
24+
----
25+
[
26+
{
27+
"extension": "revapi.reporter.text",
28+
"configuration": {
29+
"minSeverity": "BREAKING",
30+
"output": "report.txt"
31+
}
32+
}
33+
]
34+
----
35+
====
36+
1237
include::partial$Publishing_Footer.adoc[]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!--
2+
3+
Copyright 2014-2017 Lukas Krejci
4+
and other contributors as indicated by the @author tags.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
-->
19+
<configuration>
20+
21+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
22+
<encoder>
23+
<pattern>%msg%n</pattern>
24+
</encoder>
25+
</appender>
26+
27+
<!-- change to "trace" if AST debugging is needed -->
28+
<root level="info">
29+
<appender-ref ref="STDOUT"/>
30+
</root>
31+
</configuration>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
{
3+
"extension": "revapi.reporter.text",
4+
"configuration": {
5+
"minSeverity": "BREAKING"
6+
}
7+
}
8+
]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public class Visibility {
2+
public int f;
3+
4+
private class SuperClass {
5+
public int f;
6+
}
7+
8+
public class SubClass extends SuperClass {
9+
private int f2;
10+
}
11+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//// SNIPPET:BUILD
2+
package build
3+
import mill._, javalib._, publish._, revapi._
4+
5+
object bar extends JavaModule with RevapiModule {
6+
def publishVersion = "0.0.1"
7+
8+
def pomSettings = PomSettings(
9+
description = "Hello",
10+
organization = "com.lihaoyi",
11+
url = "https://github.com/lihaoyi/example",
12+
licenses = Seq(License.MIT),
13+
versionControl = VersionControl.github("lihaoyi", "example"),
14+
developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
15+
)
16+
17+
override def revapiConfigFiles: T[Seq[PathRef]] =
18+
// add Revapi config JSON file(s)
19+
Task.Sources(millSourcePath / "conf/revapi.json")
20+
21+
override def revapiClasspath: T[Agg[PathRef]] = T {
22+
// add folder containing logback.xml
23+
super.revapiClasspath() ++ Seq(PathRef(millSourcePath / "conf"))
24+
}
25+
}
26+
27+
// This example uses the `revapi` task, provided by the `RevapiModule`, to run an
28+
// analysis on old and new archives of a module to identify incompatibilities.
29+
//
30+
// NOTE: For demonstration purposes, an archive, to compare against, is published locally.
31+
// In real usage, the old version would be downloaded from the publish repository.
32+
33+
/** Usage
34+
35+
> mill bar.publishLocal
36+
Publishing Artifact(com.lihaoyi,bar,0.0.1) to ivy repo...
37+
38+
> cp dev/src/Visibility.java bar/src/Visibility.java
39+
40+
> mill bar.revapi
41+
Starting analysis
42+
Analysis results
43+
----------------
44+
old: field Visibility.SuperClass.f @ Visibility.SubClass
45+
new: <none>
46+
java.field.removed: Field removed from class.
47+
... BREAKING
48+
old: field Visibility.f
49+
new: field Visibility.f
50+
java.field.visibilityReduced: Visibility was reduced from 'public' to 'protected'.
51+
... BREAKING
52+
Analysis took ...ms.
53+
*/
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public class Visibility {
2+
protected int f;
3+
4+
private class SuperClass {
5+
private int f;
6+
}
7+
8+
public class SubClass extends SuperClass {
9+
public int f2;
10+
}
11+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package mill.javalib.revapi
2+
3+
import mill._
4+
import mill.javalib._
5+
import mill.javalib.revapi.RevapiModule.optional
6+
import mill.scalalib.publish.Artifact
7+
import mill.util.Jvm
8+
9+
/**
10+
* Adds support for [[https://revapi.org/revapi-site/main/index.html Revapi checker]] to enable API analysis and change tracking.
11+
*/
12+
@mill.api.experimental // until Revapi has a stable release
13+
trait RevapiModule extends PublishModule {
14+
15+
/**
16+
* Runs [[revapiCliVersion Revapi CLI]] on this module's archives.
17+
*
18+
* @param args additional CLI options
19+
* @return CLI working directory
20+
*/
21+
def revapi(args: String*): Command[PathRef] = Task.Command {
22+
val workingDir = T.dest
23+
24+
val oldFiles = revapiOldFiles()
25+
val oldFile = oldFiles.head
26+
val oldSupFiles = oldFiles.tail
27+
28+
val newFiles = revapiNewFiles()
29+
val newFile = newFiles.head
30+
val newSupFiles = newFiles.tail
31+
32+
val mainClass = "org.revapi.standalone.Main"
33+
val mainArgs =
34+
Seq.newBuilder[String]
35+
// https://github.com/revapi/revapi/blob/69445626881347fbf7811a4a78ff230fe152a2dc/revapi-standalone/src/main/java/org/revapi/standalone/Main.java#L149
36+
.++=(Seq(mainClass, workingDir.toString()))
37+
// https://github.com/revapi/revapi/blob/69445626881347fbf7811a4a78ff230fe152a2dc/revapi-standalone/src/main/java/org/revapi/standalone/Main.java#L97
38+
.++=(Seq("-e", revapiExtensions().mkString(",")))
39+
.++=(Seq("-o", oldFile.path.toString()))
40+
.++=(optional("-s", oldSupFiles.iterator.map(_.path)))
41+
.++=(Seq("-n", newFile.path.toString()))
42+
.++=(optional("-t", newSupFiles.iterator.map(_.path)))
43+
.++=(optional("-c", revapiConfigFiles().iterator.map(_.path)))
44+
.++=(Seq("-d", revapiCacheDir().path.toString()))
45+
.++=(optional("-r", revapiRemoteRepositories()))
46+
.++=(args)
47+
.result()
48+
49+
T.log.info("running revapi cli")
50+
Jvm.runSubprocess(
51+
mainClass = mainClass,
52+
classPath = revapiClasspath().map(_.path),
53+
jvmArgs = revapiJvmArgs(),
54+
mainArgs = mainArgs,
55+
workingDir = workingDir
56+
)
57+
58+
PathRef(workingDir)
59+
}
60+
61+
/**
62+
* List of Maven GAVs of Revapi extensions
63+
*
64+
* @note Must be non-empty.
65+
*/
66+
def revapiExtensions: T[Seq[String]] = Seq(
67+
"org.revapi:revapi-java:0.28.1",
68+
"org.revapi:revapi-reporter-text:0.15.0"
69+
)
70+
71+
/** API archive and supplement files (dependencies) to compare against */
72+
def revapiOldFiles: T[Agg[PathRef]] = T {
73+
val Artifact(group, id, version) = publishSelfDependency()
74+
defaultResolver().resolveDeps(
75+
Seq(ivy"$group:$id:$version"),
76+
artifactTypes = Some(revapiArtifactTypes())
77+
)
78+
}
79+
80+
/** API archive and supplement files (dependencies) to compare */
81+
def revapiNewFiles: T[Agg[PathRef]] = T {
82+
Agg(jar()) ++
83+
T.traverse(recursiveModuleDeps)(_.jar)() ++
84+
defaultResolver().resolveDeps(
85+
transitiveIvyDeps(),
86+
artifactTypes = Some(revapiArtifactTypes())
87+
)
88+
}
89+
90+
/** List of configuration files */
91+
def revapiConfigFiles: T[Seq[PathRef]] = Seq.empty[PathRef]
92+
93+
/** Location of local cache of extensions to use to locate artifacts */
94+
def revapiCacheDir: T[PathRef] = T { PathRef(T.dest) }
95+
96+
/** URLs of remote Maven repositories to use for artifact resolution */
97+
def revapiRemoteRepositories: T[Seq[String]] = T {
98+
repositoriesTask()
99+
.collect { case repo: coursier.MavenRepository => repo.root }
100+
}
101+
102+
/** Classpath containing the Revapi [[revapiCliVersion CLI]] */
103+
def revapiClasspath: T[Agg[PathRef]] = T {
104+
defaultResolver().resolveDeps(
105+
Agg(ivy"org.revapi:revapi-standalone:${revapiCliVersion()}")
106+
)
107+
}
108+
109+
/** [[https://revapi.org/revapi-standalone/0.12.0/index.html Revapi CLI]] version */
110+
def revapiCliVersion: T[String] = "0.12.0"
111+
112+
/** JVM arguments for the Revapi [[revapiCliVersion CLI]] */
113+
def revapiJvmArgs: T[Seq[String]] = Seq.empty[String]
114+
115+
/** Artifact types to resolve archives and supplement files (dependencies) */
116+
def revapiArtifactTypes: T[Set[coursier.Type]] = Set(coursier.Type.jar)
117+
}
118+
@mill.api.experimental
119+
object RevapiModule {
120+
121+
private def optional[T](name: String, values: IterableOnce[T]): Seq[String] = {
122+
val it = values.iterator
123+
if (it.isEmpty) Seq.empty
124+
else Seq(name, it.mkString(","))
125+
}
126+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!--
2+
3+
Copyright 2014-2017 Lukas Krejci
4+
and other contributors as indicated by the @author tags.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
-->
19+
<configuration>
20+
21+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
22+
<encoder>
23+
<pattern>%msg%n</pattern>
24+
</encoder>
25+
</appender>
26+
27+
<!-- change to "trace" if AST debugging is needed -->
28+
<root level="info">
29+
<appender-ref ref="STDOUT"/>
30+
</root>
31+
</configuration>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{
3+
"extension": "revapi.reporter.text",
4+
"configuration": {
5+
"minSeverity": "BREAKING",
6+
"output": "report.txt"
7+
}
8+
}
9+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import java.lang.annotation.ElementType;
2+
import java.lang.annotation.Target;
3+
4+
@Target(ElementType.TYPE)
5+
public @interface InheritedAnnotation {}

0 commit comments

Comments
 (0)