Skip to content

Conversation

@FusionBrah
Copy link

@FusionBrah FusionBrah commented Jan 7, 2026

Adds ShadingModule and ShadingPublishModule traits for dependency shading (relocation).

Usage

object foo extends JavaModule with ShadingModule {
  def shadedMvnDeps = Seq(mvn"com.google.code.gson:gson:2.10.1")
  def shadeRelocations = Seq(("com.google.gson.**", "shaded.gson.@1"))
}

For publishing (filters shaded deps from POM):

object foo extends JavaModule with ShadingPublishModule { ... }

For Scala modules, import from mill.javalib:

import mill.javalib.ShadingModule
object foo extends ScalaModule with ShadingModule { ... }

What Gets Shaded

Dependency Type Bundled in JAR? In Published POM?
mvnDeps No Yes
shadedMvnDeps Yes (relocated) No
Transitives of mvnDeps No Yes
Transitives of shadedMvnDeps Yes (relocated) No
moduleDeps No N/A

Only shadedMvnDeps and their transitives are vendored. moduleDeps are unaffected.

Comparison with Maven/Gradle/SBT

Gradle Shadow (closest to this approach):

  • Uses implementation (bundled, excluded from POM) vs shadow (in POM, not bundled)
  • Mill's shadedMvnDeps ≈ Gradle's implementation in shadow context

Maven shade-plugin:

  • Shades from compile scope with <includes>/<excludes> filters
  • More config than Mill's explicit separation

SBT sbt-assembly:

  • Shading only during assembly, not regular package
  • Mill shades in jar to support library publishing

Design

Reuses Assembly.Rule.Relocate and jarjar-abrams for bytecode transformation:

  1. shadedMvnDeps resolved with transitives → resolvedShadedDeps
  2. shadedJar applies relocation via jarjar-abrams
  3. Overrides compileClasspath/runClasspath/localClasspath to swap original JARs for shaded JAR
  4. ShadingPublishModule filters shaded artifacts from publishXmlDeps

Why two traits? ShadingModule handles the shading mechanics. ShadingPublishModule adds POM filtering. Separating them allows shading for internal use (assembly, run) without requiring publish setup.

Limitations

  • Shading is all-or-nothing per dependency (no partial class filtering)
  • All transitives of shadedMvnDeps are shaded together
  • Source code must use relocated package names
  • If a dependency appears in both mvnDeps and shadedMvnDeps, behavior is undefined (user should avoid this)
  • If moduleDeps contains a ShadingModule, each module shades independently (no composition)

Alternatives Considered

  • Shade only at assembly time: Doesn't help library publishing use case
  • Keep original imports, relocate at runtime: Hides conflicts until deployment
  • New shading infrastructure: More maintenance; reusing Assembly + jarjar-abrams is consistent

Closes #3815

FusionBrah and others added 5 commits January 7, 2026 16:21
This implements comprehensive shading support for Mill:
- ShadingModule trait with shadedMvnDeps, shadeRelocations, shadedJar tasks
- ShadingPublishModule for publishing with shaded deps filtered from POM
- ScalaShadingModule and ScalaShadingPublishModule for Scala projects
- Example builds for both Java and Scala
- Unit tests for shading functionality
- Documentation updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add compileClasspath override to include shadedJar, allowing source
  code to import shaded package names (e.g., `import shaded.gson.Gson`)
- Fix jar task to use Assembly.create instead of Jvm.createJar, which
  properly extracts and merges shaded JAR contents instead of adding
  the JAR as an opaque file
- Update example build.mill files with proper SNIPPET markers so Java
  example correctly overrides Scala upstream's test commands

Fixes the shading implementation for issue com-lihaoyi#3815.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@FusionBrah
Copy link
Author

CI Failure Analysis

The 3 failing jobs appear to be infrastructure issues unrelated to this PR:

Job Issue
libs.{scalalib,init,tabcomplete}.__.test Network error downloading scala-library-3.8.0-RC1-bin-...-NIGHTLY.jar - SocketException: Connection reset
example.{cli,fundamentals,depth,extending,large}.__.shared.daemon Scalafmt error in generated code (generatedScriptSources.dest/wrapped/build_/build.mill), not source files
windows libs.{util,javalib,androidlib,graphviz}.__.test Windows file locking issue - "file being used by another process"

The shading-specific tests all passed:

  • linux example.javalib.__.shared.daemon
  • linux example.scalalib.__.shared.daemon

Happy to address any actual issues if they come up after a re-run.

@lihaoyi
Copy link
Member

lihaoyi commented Jan 7, 2026

Please write a proper PR description explaining what the new feature is and how to use it

@FusionBrah
Copy link
Author

Please write a proper PR description explaining what the new feature is and how to use it

I have updated the PR accordingly. Let me know if you need anything else.

@lihaoyi
Copy link
Member

lihaoyi commented Jan 7, 2026

Please cut all the AI slop from the PR description, it is unhelpful

@lihaoyi
Copy link
Member

lihaoyi commented Jan 7, 2026

One thing still missing is an explanation of the design and limitations of your PR and what alternatives were considered and why this one was chosen

@lihaoyi
Copy link
Member

lihaoyi commented Jan 7, 2026

What happens if you publish a ShadingModule? Do its mvnDeps get vendored? what about its moduleDeps? and what about the transitive versions of each?

@lihaoyi
Copy link
Member

lihaoyi commented Jan 7, 2026

Could you comment on how Maven/Gradle/SBT manage shading, and how your approach implemented here compares to what they do

@FusionBrah
Copy link
Author

Let me know if you need any further changes :)

@lefou
Copy link
Member

lefou commented Jan 8, 2026

Why are the ScalaShadingModule and ScalaShadingPublishModule traits needed?

…raits

Scala users can just import from mill.javalib directly, consistent with
how PublishModule works (no ScalaPublishModule exists).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@FusionBrah
Copy link
Author

They're not needed. Removed them - Scala users can just import mill.javalib.ShadingModule and use ScalaModule with ShadingModule. This is consistent with how PublishModule works (no ScalaPublishModule exists).

autofix-ci bot and others added 2 commits January 8, 2026 10:55
- Add validation warning when shadedMvnDeps is non-empty but shadeRelocations is empty
- Add tests for compileClasspath override
- Add tests for ShadingPublishModule.publishXmlDeps filtering
- Add tests using ShadingNoRelocationsModule
- Add foo.run to example tests for runtime verification
- Fix scaladoc accuracy in ShadingPublishModule (jar override, not localClasspath)
- Improve documentation for shadeRelocations (wildcard syntax, edge case behavior)
- Improve documentation for compileClasspath (clarify source code MUST use relocated names)
- Revert spurious trailing newline in ScalaModule.scala

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@FusionBrah
Copy link
Author

Pushed additional improvements:

  • Added validation warning when shadedMvnDeps is set but shadeRelocations is empty
  • Added tests for compileClasspath, publishXmlDeps, and no-relocations edge case
  • Added foo.run to examples to verify runtime behavior
  • Fixed scaladoc accuracy and improved documentation

@lihaoyi
Copy link
Member

lihaoyi commented Jan 8, 2026

I'm thinking whether or not it's better for ShadingModule to be its own standalone thing, similar to BomModule, rather than being "squished" into the downstream ScalaModule/JavaModule that depends on it.

In that world, ShadingModule would have no sources, and downstream modules would depend on the ShadingModule via normal ModuleDeps. the ShadingModule would bundle its shadedMvnDeps into its jar, while its mvnDeps would remain normal maven dependencies in the final publish artifact

What do you think?

@FusionBrah
Copy link
Author

Interesting idea. The standalone approach would give cleaner separation and follows the BomModule pattern. A few questions/thoughts:

Pros I see:

  • Reusable: multiple modules could depend on the same ShadingModule
  • Cleaner separation: shading logic isolated from application code
  • Explicit: clear what's being shaded vs normal deps

Questions:

  1. Publishing: Would the ShadingModule be published as a separate artifact, or would the downstream module's jar include the shaded classes? If separate, consumers would need to depend on both artifacts.

  2. Common use case: For the typical scenario (library wanting to shade one internal dependency), would requiring two modules feel heavyweight? e.g.:

    object shadedGson extends ShadingModule { ... }
    object myLib extends JavaModule {
      def moduleDeps = Seq(shadedGson)
    }

    vs current:

    object myLib extends JavaModule with ShadingModule { ... }
  3. Imports: Would source code in the downstream module use relocated package names (import shaded.gson.Gson), or would there be some mechanism to use original names?

I can see the standalone approach being cleaner architecturally. Happy to refactor if you think that's the better direction.

@lihaoyi
Copy link
Member

lihaoyi commented Jan 9, 2026

How do Maven/Gradle/SBT's shading work with upstream dependencies and publishing?

@lihaoyi
Copy link
Member

lihaoyi commented Jan 9, 2026

Also please link to your sources in the maven/gradle/sbt docs

@FusionBrah
Copy link
Author

How Maven/Gradle/SBT Handle Shading & Publishing

Maven Shade Plugin

Approach: Shading is configured directly in the module being published (same as current PR).

POM Handling: Automatically generates a dependency-reduced-pom.xml that excludes shaded dependencies. Key options:

  • createDependencyReducedPom=true (default) - removes shaded deps from POM
  • promoteTransitiveDependencies=true - promotes transitive deps of removed dependencies to direct deps
  • useDependencyReducedPomInJar=true - embeds reduced POM in JAR (since 3.3.0)

Docs:


Gradle Shadow Plugin

Approach: Shading configured in the module being published via shadowJar task (same as current PR).

POM Handling: Uses separate dependency configurations:

  • implementation deps → bundled into JAR, excluded from POM
  • shadow deps → not bundled, declared as runtime deps in POM
  • compileOnly deps → excluded from both

The shadow component automatically handles POM generation when publishing.

Docs:


SBT Assembly

Approach: Recommends a separate subproject for shading (similar to your suggestion).

From the sbt-assembly README:

"We discourage you from publishing non-shaded über JARs beyond deployment... you would likely need to set up a front business to lie about what dependencies you have in pom.xml and ivy.xml. To do so, make a subproject for über JAR purpose only where you depend on the dependencies, and make a second cosmetic subproject that you use only for publishing."

POM Handling: No automatic POM generation - requires manual pomPostProcess XML transformation.

Docs:


Summary

Tool Architecture POM Handling
Maven Mixed into main module Automatic dependency-reduced-pom.xml
Gradle Mixed into main module Automatic via shadow component
SBT Separate subproject recommended Manual pomPostProcess

The current PR follows the Maven/Gradle pattern. The standalone ShadingModule approach you suggested aligns with sbt-assembly's recommendation.

Trade-off: The mixed approach (current PR) is simpler for the common case. The standalone approach offers better reusability and separation but requires more boilerplate for simple use cases.

Happy to refactor to the standalone pattern if you think that's the better direction for Mill.

@lefou
Copy link
Member

lefou commented Jan 9, 2026

IIUC, the current implementation can already be used for both scenarious. The user either mixes ShadingModule into her module directly, or creates a sub-module just for shading, and configures publishing only for that sub-module. Or do I overlook something?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flesh out support for shading third party libraries library and add example docs (500USD Bounty)

3 participants