Skip to content

Commit

Permalink
Add support for project settings file
Browse files Browse the repository at this point in the history
  • Loading branch information
wleczny committed Aug 30, 2022
1 parent ceafb01 commit 05be8f7
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 39 deletions.
4 changes: 3 additions & 1 deletion modules/build/src/main/scala/scala/build/CrossSources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,9 @@ object CrossSources {
lazy val subPath = sourcePath.subRelativeTo(dir)
if (os.isDir(sourcePath))
Right(Inputs.singleFilesFromDirectory(Inputs.Directory(sourcePath), enableMarkdown))
else if (sourcePath.ext == "scala") Right(Seq(Inputs.ScalaFile(dir, subPath)))
else if (sourcePath == os.sub / "project.settings.scala")
Right(Seq(Inputs.SettingsScalaFile(dir, subPath)))
else if (sourcePath.ext == "scala") Right(Seq(Inputs.SourceScalaFile(dir, subPath)))
else if (sourcePath.ext == "sc") Right(Seq(Inputs.Script(dir, subPath)))
else if (sourcePath.ext == "java") Right(Seq(Inputs.JavaFile(dir, subPath)))
else if (sourcePath.ext == "md") Right(Seq(Inputs.MarkdownFile(dir, subPath)))
Expand Down
79 changes: 58 additions & 21 deletions modules/build/src/main/scala/scala/build/Inputs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import scala.build.options.Scope
import scala.build.preprocessing.ScopePath
import scala.util.Properties
import scala.util.matching.Regex
import scala.build.Inputs.Element

final case class Inputs(
elements: Seq[Inputs.Element],
Expand Down Expand Up @@ -187,15 +188,20 @@ object Inputs {
}
sealed trait Compiled extends Element
sealed trait AnyScalaFile extends Compiled
sealed trait ScalaFile extends AnyScalaFile {
def base: os.Path
def subPath: os.SubPath
def path: os.Path = base / subPath
}

final case class Script(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile with AnyScalaFile with AnyScript {
lazy val path: os.Path = base / subPath
}
final case class ScalaFile(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile with AnyScalaFile {
lazy val path: os.Path = base / subPath
}
final case class SourceScalaFile(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile with ScalaFile
final case class SettingsScalaFile(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile with ScalaFile
final case class JavaFile(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile with Compiled {
lazy val path: os.Path = base / subPath
Expand Down Expand Up @@ -233,8 +239,10 @@ object Inputs {
.collect {
case p if p.last.endsWith(".java") =>
Inputs.JavaFile(d.path, p.subRelativeTo(d.path))
case p if p.last == "project.settings.scala" =>
Inputs.SettingsScalaFile(d.path, p.subRelativeTo(d.path))
case p if p.last.endsWith(".scala") =>
Inputs.ScalaFile(d.path, p.subRelativeTo(d.path))
Inputs.SourceScalaFile(d.path, p.subRelativeTo(d.path))
case p if p.last.endsWith(".sc") =>
Inputs.Script(d.path, p.subRelativeTo(d.path))
case p if p.last.endsWith(".md") && enableMarkdown =>
Expand All @@ -244,6 +252,18 @@ object Inputs {
.sortBy(_.subPath.segments)
}

def projectSettingsFiles(elements: Seq[Inputs.Element]): Seq[Inputs.SettingsScalaFile] =
elements.flatMap {
case f: SettingsScalaFile => Seq(f)
case d: Directory => Inputs.configFileFromDirectory(d)
case _ => Nil
}.distinct

def configFileFromDirectory(d: Inputs.Directory): Seq[Inputs.SettingsScalaFile] =
if (os.exists(d.path / "project.settings.scala"))
Seq(Inputs.SettingsScalaFile(d.path, os.sub / "project.settings.scala"))
else Nil

private def inputsHash(elements: Seq[Element]): String = {
def bytes(s: String): Array[Byte] = s.getBytes(StandardCharsets.UTF_8)
val it = elements.iterator.flatMap {
Expand All @@ -252,7 +272,8 @@ object Inputs {
case _: Inputs.Directory => "dir:"
case _: Inputs.ResourceDirectory => "resource-dir:"
case _: Inputs.JavaFile => "java:"
case _: Inputs.ScalaFile => "scala:"
case _: Inputs.SettingsScalaFile => "config:"
case _: Inputs.SourceScalaFile => "scala:"
case _: Inputs.Script => "sc:"
case _: Inputs.MarkdownFile => "md:"
}
Expand Down Expand Up @@ -285,22 +306,37 @@ object Inputs {

assert(validElems.nonEmpty)

val (inferredWorkspace, inferredNeedsHash, workspaceOrigin) = validElems
.collectFirst {
case d: Directory => (d.path, true, WorkspaceOrigin.SourcePaths)
val (inferredWorkspace, inferredNeedsHash, workspaceOrigin) = {
val settingsFiles = projectSettingsFiles(validElems)
val dirsAndFiles = validElems.collect {
case d: Directory => d
case f: SourceFile => f
}
.getOrElse {
validElems.head match {
case elem: SourceFile => (elem.path / os.up, true, WorkspaceOrigin.SourcePaths)
case _: Virtual =>
val dir = homeWorkspace(validElems, directories)
(dir, false, WorkspaceOrigin.HomeDir)
case r: ResourceDirectory =>
// Makes us put .scala-build in a resource directory :/
(r.path, true, WorkspaceOrigin.ResourcePaths)
case _: Directory => sys.error("Can't happen")

settingsFiles.headOption.map { s =>
if (settingsFiles.length > 1)
System.err.println(
s"Warning: more than one project.settings.scala file has been found. Setting ${s.base} as the project root directory for this run."
)
(s.base, true, WorkspaceOrigin.SourcePaths)
}.orElse {
dirsAndFiles.collectFirst {
case d: Directory =>
if (dirsAndFiles.length > 1)
System.err.println(
s"Warning: setting ${d.path} as the project root directory for this run."
)
(d.path, true, WorkspaceOrigin.SourcePaths)
case f: SourceFile =>
if (dirsAndFiles.length > 1)
System.err.println(
s"Warning: setting ${f.path / os.up} as the project root directory for this run."
)
(f.path / os.up, true, WorkspaceOrigin.SourcePaths)
}
}
}.getOrElse((os.pwd, true, WorkspaceOrigin.Forced))
}

val (workspace, needsHash, workspaceOrigin0) = forcedWorkspace match {
case None => (inferredWorkspace, inferredNeedsHash, workspaceOrigin)
case Some(forcedWorkspace0) =>
Expand Down Expand Up @@ -420,8 +456,9 @@ object Inputs {
List(resolve(url, content))
}
}
else if (path.last == "project.settings.scala") Right(Seq(SettingsScalaFile(dir, subPath)))
else if (arg.endsWith(".sc")) Right(Seq(Script(dir, subPath)))
else if (arg.endsWith(".scala")) Right(Seq(ScalaFile(dir, subPath)))
else if (arg.endsWith(".scala")) Right(Seq(SourceScalaFile(dir, subPath)))
else if (arg.endsWith(".java")) Right(Seq(JavaFile(dir, subPath)))
else if (arg.endsWith(".md")) Right(Seq(MarkdownFile(dir, subPath)))
else if (os.isDir(path)) Right(Seq(Directory(path)))
Expand Down
82 changes: 82 additions & 0 deletions modules/build/src/test/scala/scala/build/tests/InputsTests.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
package scala.build.tests

import com.eed3si9n.expecty.Expecty.expect
import scala.build.Build
import scala.build.Inputs
import scala.build.options.{BuildOptions, InternalOptions, MaybeScalaVersion}
import scala.build.tests.util.BloopServer
import scala.build.{BuildThreads, Directories, LocalRepo}
import scala.build.internal.Constants

class InputsTests extends munit.FunSuite {
val buildThreads = BuildThreads.create()
val extraRepoTmpDir = os.temp.dir(prefix = "scala-cli-tests-extra-repo-")
val directories = Directories.under(extraRepoTmpDir)
def bloopConfigOpt = Some(BloopServer.bloopConfig)
val buildOptions = BuildOptions(
internal = InternalOptions(
localRepository = LocalRepo.localRepo(directories.localRepoDir),
keepDiagnostics = true
)
)

test("forced workspace") {
val testInputs = TestInputs(
Expand All @@ -21,4 +37,70 @@ class InputsTests extends munit.FunSuite {
}
}

test("project settings file") {
val testInputs = TestInputs(
files = Seq(
os.rel / "custom-dir" / "project.settings.scala" -> "",
os.rel / "project.settings.scala" -> s"//> using javaProp \"foo=bar\"".stripMargin,
os.rel / "foo.scala" ->
s"""object Foo {
| def main(args: Array[String]): Unit =
| println("Foo")
|}
|""".stripMargin
),
inputArgs = Seq("custom-dir", "foo.scala", "project.settings.scala")
)
testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) {
(root, _, buildMaybe) =>
val javaOptsCheck = buildMaybe match {
case Right(build: Build.Successful) =>
build.options.javaOptions.javaOpts.toSeq(0).value.value == "-Dfoo=bar"
case _ => false
}
assert(javaOptsCheck)
assert(os.exists(root / "custom-dir" / Constants.workspaceDirName))
assert(!os.exists(root / Constants.workspaceDirName))
}
}

test("setting root dir without project settings file") {
val testInputs = TestInputs(
files = Seq(
os.rel / "custom-dir" / "foo.scala" ->
s"""object Foo {
| def main(args: Array[String]): Unit =
| println("Foo")
|}
|""".stripMargin,
os.rel / "bar.scala" -> ""
),
inputArgs = Seq("custom-dir", "bar.scala")
)
testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) {
(root, _, _) =>
assert(os.exists(root / "custom-dir" / Constants.workspaceDirName))
assert(!os.exists(root / Constants.workspaceDirName))
}
}

test("passing project settings file and its parent directory") {
val testInputs = TestInputs(
files = Seq(
os.rel / "foo.scala" ->
s"""object Foo {
| def main(args: Array[String]): Unit =
| println("Foo")
|}
|""".stripMargin,
os.rel / "project.settings.scala" -> ""
),
inputArgs = Seq(".", "project.settings.scala")
)
testInputs.withBuild(buildOptions, buildThreads, bloopConfigOpt) {
(root, inputs, _) =>
assert(os.exists(root / Constants.workspaceDirName))
assert(Inputs.projectSettingsFiles(inputs.elements).length == 1)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class PreprocessingTests extends munit.FunSuite {

test("Report error if scala file not exists") {
val logger = TestLogger()
val scalaFile = Inputs.ScalaFile(os.temp.dir(), os.SubPath("NotExists.scala"))
val scalaFile = Inputs.SourceScalaFile(os.temp.dir(), os.SubPath("NotExists.scala"))

val res = ScalaPreprocessor.preprocess(scalaFile, logger, withRestrictedFeatures = false)
val expectedMessage = s"File not found: ${scalaFile.path}"
Expand Down
4 changes: 2 additions & 2 deletions modules/cli/src/main/scala/scala/cli/commands/Fmt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ object Fmt extends ScalaCommand[FmtOptions] {
else {
val i = options.shared.inputs(args.all).orExit(logger)
val s = i.sourceFiles().collect {
case sc: Inputs.Script => sc.path
case sc: Inputs.ScalaFile => sc.path
case sc: Inputs.Script => sc.path
case sc: Inputs.SourceScalaFile => sc.path
}
(s, i.workspace, Some(i))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,14 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
}
}

test("setting root dir with no inputs") {
val url = "https://gist.github.com/alexarchambault/7b4ec20c4033690dd750ffd601e540ec"
emptyInputs.fromRoot { root =>
os.proc(TestUtil.cli, extraOptions, escapedUrls(url)).call(cwd = root)
expect(os.exists(root / ".scala-build"))
}
}

private lazy val ansiRegex = "\u001B\\[[;\\d]*m".r
private def stripAnsi(s: String): String =
ansiRegex.replaceAllIn(s, "")
Expand Down
60 changes: 60 additions & 0 deletions website/docs/reference/root-dir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Project root directory
sidebar_position: 5
---

## Usage

Scala CLI needs a root directory:
- to write mapped sources
- to write class files
- for Bloop

## Setting root directory

First of all, Scala CLI checks every passed input (in the same order in which inputs were passed) for the `project.settings.scala` file:
- If the `project.settings.scala` file is passed explicitly as a **source**, Scala CLI sets its parent directory as the root directory.
- If the input is a **directory**, Scala CLI looks for the `project.settings.scala` inside this directory. If the file is found, Scala CLI sets the passed directory as the root directory.

If more than one `project.settings.scala` file is found, Scala CLI uses only **the first one** to set the root directory and raises **warning** saying which one was used.

If no `project.settings.scala` files are found, Scala CLI sets the root directory based on the first file/directory input:
- If the input is a **directory**, it is set as the root directory.
- If the input is a **file**, Scala CLI sets its parent directory as the root directory.

If more then one file/directory input has ben passed Scala CLI raises the warning saying which directory has been set as the project root directory.

If no `project.settings.scala` files are found and no file/directory inputs have ben passed, Scala CLI sets the current working directory (where Scala CLI was invoked from) as the project root directory.

#### Example

Let's say we have the following file structure:

```
project
│ project.settings.scala
└───dir1
│ │ file1.scala
│ │
│ └───dir2
│ │ project.settings.scala
│ │ file2.scala
└───dir3
│ project.settings.scala
│ file3.scala
```

And user runs the following command:
```
project> scala-cli dir1/file1.scala dir1/dir2 dir3/project.settings.scala
```

Scala CLI will find 2 `project.settings.scala` files:
- inside `dir2`, since this directory was passed as an input and it has `project.settings.scala` inside.
- inside `dir3`, since `dir3/project.settings.scala` was passed explicitly as a source

`dir1/dir2` was passed before `dir3/project.settings.scala`, so `dir2` will be set as the **root** directory for this build.

Since more than one `project.settings.scala` has been found, Scala CLI will raise the warning saying that more than one `project.settings.scala` file has been found and `dir1/dir2` has been set as the project root directory.
14 changes: 0 additions & 14 deletions website/docs/reference/working-dir.md

This file was deleted.

0 comments on commit 05be8f7

Please sign in to comment.