Skip to content

joan38/moulin

Repository files navigation

Moulin

Build multi-projects with Scala CLI.

Demo.mp4

Getting Started

Tip

Before using Moulin for your project I would recommend using the scala CLI to start with.
Once scala CLI is too limited for your project (need for multi modules, use plugins...) then this is where Moulin can help:

If not done already, create a folder for your project and cd in it:

mkdir my-project && cd $_

If not done already, download a standalone Scala launcher in your project:

curl -o scala https://raw.githubusercontent.com/VirtusLab/scala-cli/main/scala-cli.sh && chmod +x scala

Assuming you have/want the following project structure:

my-project
├─ app1
├─ app2
└─ common

where app1 and app2 depend on common.

Create build.scala as follows:

#!/usr/bin/env -S ./scala shebang --workspace=.scala-build/moulin --semanticdb-sourceroot=.
//> using dep com.goyeau::moulin:0.1.0-SNAPSHOT
import com.goyeau.moulin.*

// An object that will contain the Moulin definition
object `my-project` extends Moulin:
  // An object for the Scala module in the app1 folder
  object app1 extends MyProjectModule:
    override def dependsOn = super.dependsOn :+ common

  // An object for the Scala module in the app2 folder
  object app2 extends MyProjectModule:
    override def dependsOn = super.dependsOn :+ common

  // An object for the Scala module in the common folder
  object common extends MyProjectModule

  // A trait to factor out common settings
  trait MyProjectModule extends ScalaModule:
    override def scalaVersion = "3.5.0"

Make it executable:

chmod +x build.scala

Now to compile all modules run:

./build.scala all.compile

Compile common only:

./build.scala common.compile

Compile app1 and it's recursive dependencies (common) only:

./build.scala app1.compile

Command line and options

The command line is nothing else than Scala code. This means the following works:

./build.scala 'println("Hello world")'

So when you run ./build.scala common.compile, there is literaly a function compile on the object common.
This is the same with ./build.scala all.compile, there is a function called all that will run compile on all the modules that have compile defined on them.

IDE support

Moulin supports any IDE that supports BSP. For ScalaModules Moulin simply forwards scala cli's BSP server.

To setup the BSP config run the following command before openning the project in your IDE:

./build.scala bsp.setup

Plugins

Here is an example with scalac-options:

#!/usr/bin/env -S ./scala shebang --workspace=.scala-build/moulin --semanticdb-sourceroot=.
//> using dep com.goyeau::moulin:0.1.0-SNAPSHOT
//> using dep org.typelevel::scalac-options:0.1.7
import com.goyeau.moulin.*
import org.typelevel.scalacoptions.ScalacOptions.*
import org.typelevel.scalacoptions.{ScalaVersion, ScalacOptions}

object `my-project` extends Moulin:
  object app1 extends MyProjectModule:
    override def dependsOn = super.dependsOn :+ common

  object app2 extends MyProjectModule:
    override def dependsOn = super.dependsOn :+ common

  object common extends MyProjectModule

  trait MyProjectModule extends ScalaModule:
    override def scalaVersion  = "3.5.0"
    override def scalacOptions = super.scalacOptions ++ ScalacOptions.tokensForVersion(
      ScalaVersion.unsafeFromString(scalaVersion),
      ScalacOptions.default + sourceFutureMigration ++ fatalWarningOptions
    )

Extend Moulin

Let's create a Hello World function on our project:

object `my-project` extends Moulin:
  object app:
    def toto = println("Hello World")

We can run this function with:

./build.scala app.toto

Now we recommend caching functions so that they execute only if something changed in the build:

import com.goyeau.moulin.cache.Cache.cached

object `my-project` extends Moulin:
  object app extends ScalaModule:
    override def scalaVersion  = "3.5.0"

    def push = cached(assembly()): assembly =>
      println(s"Pushing: ${assembly.path}")

Running the push function will evict the cache only if assembly() returns a different hashCode (or if the build definition is modified).

Here is an example where we want to write to a file only if the date changed:

import java.time.LocalDate

def currentDate() = LocalDate.now()

def writeDate = cached(currentDate()): date =>
  val file = dest / "file.txt"
  println(s"New day, new date written to $file!")
  os.write(file, s"Hello, the date is $date")
  file

Why Moulin?

I love Scala 3, I love Mill, I love the scala CLI. Moulin is what happens if they all make out :D Moulin means Mill in French, so this is no surprise that Moulin's API is highly inspired from Mill.

Just like Scala CLI, you can build your projects without any dependencies, no Scala or even Java installed, nothing. Moulin is just Scala code, so there is not much to learn apart from the framework.

Contributing

I've open sourced this project early because I need help from you guys to make this a reality. Please let me know what you think (good or bad) and how can we improve this tool.

About

Build multi-projects with Scala CLI

Resources

License

Stars

Watchers

Forks

Releases

No releases published