A .jar added to the Eclipse classpath is missing at runtime

I am trying to insert a .jar file onto the classpath in Eclipse only. (Don’t ask!) It seems to only get onto the compile classpath. It builds, but when launching with Run, or Run as JUnit it is missing and crashes.

The relevant bits are:

subprojects {
    configurations {
        parserSubstitution
    }
    dependencies {
        parserSubstitution module("com.example:parser:1.0.0-SNAPSHOT")
    }
 
    apply plugin: 'eclipse'
    eclipse {
        classpath {
            plusConfigurations += [ configurations.parserSubstitution ]
        }
    }

In Eclipse this shows up in all Java projects under “Project and External Dependencies” as I would expect.

I’ve tried variations like:

     configurations {
        parserSubstitution.extendsFrom(compile)
    }

I thought (from the diagram here) that this should put it on all relevant classpaths, but then it disappears from “Project and External Dependencies” and doesn’t compile, ironically.

Is this a bug or am I missing something in my understanding / build script?

Versions:

  • Photon M5
  • Buildship 2.2.1 (also tried 3.0.0 snapshot)
  • Gradle 4.6

UPDATE:

For the complete solution I ended up with see my SO answer here, which makes use of the workaround Donát provided:

I think I found a big clue: is Buildship explicitly removing stuff under .m2 paths to avoid interoperability problems?

What I just observed:

  • I thought “let’s just add the built .jar to the launch configuration’s classpath” - as a workaround
  • It still crashed with “class not found”. Hmm…
  • look at the launch configuration dialog -> .jar has gone!
  • put it back manually
  • close & reopen the dialog -> .jar has gone again!

The Buildship classpath provider doesn’t read the custom classpath entries from the run configuration. Basically it’s an alteration from the standard classpath provider.

Buildship classpath provider doesn’t read the custom classpath entries from the run configuration

So to check if I understand:

  • “custom classpath entries” == what I see called “User Entries” in the Eclipse GUI?
  • “Buildship classpath provider” == what populates “Project and External dependencies” in the GUI

In which case, these should still be additive right? So this still doesn’t explain what’s wiping out references to jars in the .m2 repo.

To elaborate: I was able to move the problematic .jar file outside of the .m2 repo, then add it to the “User Entries”: that was then picked up and ran (and wasn’t deleted from the launch configuration anymore). Similarly I have been able to add a special “debug menu” project dependency in addition to Gradle’s dependencies for testing - that has been working fine.

Not exactly. Buildship gets a bit complicated when it comes to the classpath, since there’s no way to have a one-to-one mapping for the compilation and the execution.

For the compilation, we rely on the Eclipse compiler. Build only adds dependencies (projects and jar files) via the Project and External Dependencies classpath container. The content comes from a Tooling API model and set by the GradleClasspathContainerUpdater class.

For the execution, there are two possibilities. When you execute a Gradle build via a task then the execution happens in a separate java process which behaves exactly like the command line. When you execute Java application via JDT (Run as > Java Application) and the target is a Buildship project then the filtered version of the compile classpath is used. Buildship checks the main class being executed, looks up which Gradle source set it belongs, and filters the elements declared in a different source set.

This filtering mechanism replaces the standard classpath provider algorithm via the org.eclipse.jdt.launching.classpathProviders extension point and ignores the user entries you set on the run configuration UI.

I hope this somewhat makes sense. :wink:

OK, that’s starting to give me some dots to join. Focusing on

And in particular

So is this filtering basically what implements the main / test separation (github issue 354)?

How does this interact with the explicit dependency & configuration I defined in the example at the top? Do these get associated with a source-set? If not, what happens to such dependencies with no source-set - are they going to be filtered-out as well?

Yes.

It should work like on the command line. If a dependency is available on when compiling/running the main sources then it should be also available when executing a main() method from the same source set. If no source set information can be determined then no filtering is done (for instance when Gradle 4.4 or earlier is used or corresponding classpath attributes are not available on the Eclipse model). Dependencies with not associated source sets are excluded.

Thanks again. Final questions for now (I hope!) Prior to issue 354, would the following modify both the compile and runtime classpaths in Eclipse?

eclipse {
    classpath {
        plusConfigurations += [ configurations.parserSubstitution ]
    }
}

If so, isn’t the changed behaviour now a bit “odd” - at least, my intuition suggests that the eclipse { } block is intended to provide eclipse-specific customisations, that override the rest of the Gradle logic.

FYI: the reason I went via the plusConfigurations approach is because I didn’t know how to add dependencies in the whenMerged block. Is that possible? What I (thought) to do was to get Gradle to add these dependencies, and I could delete the ones I didn’t want there again.

Is there an example of constructing a Library dependency to add during whenMerged? (It’s constructor wants a Node and I don’t know what that means.) Otherwise I still can’t picture a viable solution to my issue.

Yes, in the previous releases the compile classpath was equal to the runtime classpath.

You can disable the runtime classpath filtering by removing the corresponding classpath attribute from the dependency.

eclipse.classpath.file.whenMerged {
    entries.each { 
        if (it.path.contains("com.example.parser")) {
            it.entryAttributes.remove("gradle_used_by_scope")
        }
    }
} 

Nagyszerű! I think I can work with that. Thanks again.

1 Like

I’m glad to hear that.