Unexpected compiler behavior when excluding files from artifacts

When trying to write a library (more on the why I need to do this at the bottom of the post), I got blocked by an unexpected behavior in the Kotlin compiler. In Java this works, and I can’t figure out why it shouldn’t work in Kotlin.

Minimal examples
All classes in the examples are in the same package (omitted for brevity).

First the Java version, which compiles and runs fine:

// ** foo-utils Maven artifact **
// The Foo class is excluded from the artifact (using maven-jar-plugin)

// Foo.java
class Foo {}

// FooUtils.java
class FooUtils {
    public static void receiveFoo(Foo foo) {}
}

// ** bar Maven artifact **
// Depends on foo-utils

// Foo.java
class Foo {}

// App.java
public class App {
  public static void main(String[] args) {
    FooUtils.receiveFoo(new Foo());
  }
}

Now the Kotlin version, which give a compilation error:

// ** foo-utils Maven artifact **
// The Foo class is excluded from the artifact (using maven-jar-plugin)

// Foo.kt
class Foo {}

// FooUtils.kt
object FooUtils {
    fun receiveFoo(foo: Foo) {}
}

// ** bar Maven artifact **
// Depends on foo-utils

// Foo.kt
class Foo {}

// App.kt
object App {
    @JvmStatic
    fun main(args: Array<String>) {
        // COMPILATION ERROR: Cannot access class 'org.example.Foo'. Check your module classpath for missing or conflicting dependencies
        FooUtils.receiveFoo(Foo())
    }
}

Is this expected? Why? Is there a way to make it work like in Java?

Why I need to do this
Kotlin projects that uses Protobuf will have Kotlin classes (generated by the Protobuf compiler) such as google.protobuf.Timestamp.

I’m writing a library with an utility class that can convert such classes. For example:

fun Timestamp.toInstant() = Instant.ofEpochSecond(this.seconds, this.nanos.toLong())

In order to compile my library, I need to have these message classes (such as google.protobuf.Timestamp) present (generated) in my project. However, I don’t want to distribute google.protobuf.Timestamp in the library artifact, since that can cause version conflicts (because these projects generates server/client stubs together with such message classes, and they’re all designed to fit together – their internal API changes often so things will break if some files are overridden by dependency).

1 Like

I don’t know why Kotlin is doing what it’s doing, but regarding your issue, couldn’t you make the libraries that your library depends on either provided or compile dependencies? Then they won’t be included in your library and thus won’t cause version conflicts.

couldn’t you make the libraries that your library depends on either provided or compile dependencies?

In the current setup, the message classes doesn’t come from a library, but are generated from proto files by each project (Timestamp was a bad example I realized, it’s actually company internal message classes).

So your suggestion wouldn’t work in the current setup. However, I’m considering changing this setup so that these message classes are built and provided as a library instead. Only downside of this would be that the library would have to stay in sync with whatever protoc version depending projects are using.

Still, would be interesting to know why Kotlin behaves in this way. Why would kotlinc care where a file comes from, as long as it’s on the classpath? :thinking:

Actually now that I think about it, I suspect that it’s related to Kotlin not having package-private visibility.

From memory, the Kotlin devs said that package-private isn’t really secure, because anyone can create a class in the same package in their own code base and then just get access to all the things in that package. So I think Kotlin kind of mangles the packages to prevent that abuse. So since you have the same package in two different libraries, when compiled, it’s not actually the same package at all.

1 Like

Well, I don’t think there is a single, correct way to handle this case. Your setup is “weird”, Java decided to handle it one way, Kotlin handles it differently - that’s it. Kotlin compiler probably assumes foo module requires a different Foo class than bar modules sees. Or something similar. Java compiler doesn’t care.

That’s likely it! I remember reading about this a long time ago, but its relation to this issue didn’t cross my mind. Thanks!

1 Like