mill icon indicating copy to clipboard operation
mill copied to clipboard

Android Release Task/Module Implementation

Open souvlakias opened this issue 1 month ago • 2 comments

Creating this issue so we can track various attempts on implementing the release module for Mill's android support. Current implementation is a hardcoded boolean task: androidIsDebug. Goal is to have a separate task or module for the release logic while also share tasks/resources between the two variants.

1. Cross Module (branch)

Have AndroidCrossModule / AndroidAppCrossModule. Note: The linked branch has AndroidModule converted into a cross module but that wasn't the best choice.

Pros

  • This enables us to have two modules Debug/Release. Internal tasks could then determine the variant based on crossValue.

Cons

  • More complex api for the user

    trait App extends AndroidAppCrossModule {
      object test extends AndroidAppInstrumentedTests
    }
    object app extends Cross[App](AndroidBuildVariant.Debug, AndroidBuildVariant.Release)
    
  • More complex cli for the user

    ./mill show app[debug].androidApk
    ./mill show app[debug].test
    
  • Tests submodules will exist for both variants but release should not really have any.

  • No shared resources between the variants

2. Shared Base Trait

Below are two attempts on having a base trait for the application.

Some common Pros/Cons are:

Pros

  • It requires no modification of androidlib's modules.
  • Tests Submodules can be defined for only the Debug variant

Cons

  • Separate resources for all tasks

2.1 2 Submodules (branch)

Have a base trait and then instantiate 2 submodules that extend that:

trait base extends AndroidAppModule {
  override def moduleDir = super.moduleDir / os.up
  ??? // All implementation tasks
}

object app extends mill.api.Module {
  object debug extends base {
    object test extends AndroidAppTests, TestModule.Junit4 
  }
  object release extends base {
    override def androidIsDebug = false
    ??? // Other Release options
  }
}

Notes

This feels like the cross module attempt but with slight differences.

  • CLI would look like:
./mill show app.debug.androidApk
./mill show app.debug.test

2.2 Release Submodule (branch)

Maybe a preferred version of the above approach.

trait base extends AndroidAppModule {
  ??? // All implementation tasks
}
object app extends base {
  object test extends AndroidAppTests, TestModule.Junit4 
  object release extends base {
    override def moduleDir = super.moduleDir / os.up
    override def androidIsDebug = false
    ??? // Other Release options
  }
}

Notes

  • This considers the app module to be the debug variant and the app.release submodule to be the release variant.

  • CLI would look like:

    ./mill show app.androidApk # Debug
    ./mill show app.release.androidApk # Release
    

3. androidReleaseApk task

Unless I'm missing something, in order to have a task androidReleaseApk we would have to:

  • apply the same logic of having two variants of debug/release tasks for other tasks.

OR

  • Restructure the Modules' tasks to pass around a debug: Boolean argument. Even if restructuring is trivial, if we made tasks to accept arguments they would need to become Task.Anons which again defeats the purpose :sweat_smile:

souvlakias avatar Oct 29 '25 12:10 souvlakias

@lefou Hi, I’m enumerating all the approaches suggested and tried here with @vaslabs to make sure we’re on the same page.
If we understand the sub-module approach you suggested correctly, we think it will fit best with the base trait approach, which basically only requires changing build.mill of the examples. What do you think?

souvlakias avatar Oct 29 '25 12:10 souvlakias

There is another option. You could prepare a trait which copies/shares everything with the outer module but still allows punctual overriding of tasks like androidIsDebug. That way, you could avoid the separate base-trait setup AND have the option to selectively change some tasks. We have something comparable in ScoverageModule, which builds exactly as it's outer module, but tweaks the scalacOptions to instrument the bytecode for coverage data collection. https://github.com/com-lihaoyi/mill/blob/f219fbeaae97240364f1a75690f53d69bd60a24d/contrib/scoverage/src/mill/contrib/scoverage/ScoverageModule.scala#L132-L195

object app extends AndroidAppModule {
  object test extends AndroidAppTests, TestModule.Junit4 
  object release extends AndroidAppVariantModule {
    override def androidIsDebug = true
  }
}

Whether we pre-select androidIsDebug = true depends largely on the name, e.g. if we name it AndroidAppReleaseModule.

object app extends AndroidAppModule {
  object test extends AndroidAppTests, TestModule.Junit4 
  object release extends AndroidAppReleaseModule
}

lefou avatar Oct 29 '25 20:10 lefou