Skip to content

Commit caf3410

Browse files
committed
Fix export failing on duplicate sources
1 parent 051bf24 commit caf3410

File tree

8 files changed

+170
-9
lines changed

8 files changed

+170
-9
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package scala.build
2+
import scala.collection.mutable
3+
object CollectionOps {
4+
extension [T](items: Seq[T]) {
5+
6+
/** Works the same standard lib's `distinct`, but only differentiates based on the key extracted
7+
* by the passed function. If more than one value exists for the same key, only the first one
8+
* is kept, the rest is filtered out.
9+
*
10+
* @param f
11+
* function to extract the key used for distinction
12+
* @tparam K
13+
* type of the key used for distinction
14+
* @return
15+
* the sequence of items with distinct [[items]].map(f)
16+
*/
17+
def distinctBy[K](f: T => K): Seq[T] =
18+
if items.length == 1 then items
19+
else
20+
val seen = mutable.HashSet.empty[K]
21+
items.filter { item =>
22+
val key = f(item)
23+
if seen(key) then false
24+
else
25+
seen += key
26+
true
27+
}
28+
}
29+
}

modules/build/src/main/scala/scala/build/CrossSources.scala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scala.build
22

33
import java.io.File
44

5+
import scala.build.CollectionOps.*
56
import scala.build.EitherCps.{either, value}
67
import scala.build.Ops.*
78
import scala.build.Positioned
@@ -266,9 +267,21 @@ object CrossSources {
266267
inputs.withElements(elements = filteredElements)
267268
)
268269

269-
val preprocessedSources =
270+
val preprocessedSources: Seq[PreprocessedSource] =
270271
(preprocessedInputFromArgs ++ preprocessedSourcesFromDirectives).distinct
271-
.pipe(sources => value(validateExcludeDirectives(sources, allInputs.workspace)))
272+
.pipe { sources =>
273+
val validatedSources: Seq[PreprocessedSource] =
274+
value(validateExcludeDirectives(sources, allInputs.workspace))
275+
val distinctSources = validatedSources.distinctBy(_.distinctPathOrSource)
276+
val diff = validatedSources.diff(distinctSources)
277+
if diff.nonEmpty then
278+
val diffString = diff.map(_.distinctPathOrSource).mkString(s"${System.lineSeparator} ")
279+
logger.message(
280+
s"""[${Console.YELLOW}warn${Console.RESET}] Skipped duplicate sources:
281+
| $diffString""".stripMargin
282+
)
283+
distinctSources
284+
}
272285

273286
val scopedRequirements = preprocessedSources.flatMap(_.scopedRequirements)
274287
val scopedRequirementsByRoot = scopedRequirements.groupBy(_.path.root)

modules/build/src/main/scala/scala/build/preprocessing/PreprocessedSource.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ sealed abstract class PreprocessedSource extends Product with Serializable {
99
def optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]]
1010
def requirements: Option[BuildRequirements]
1111
def mainClassOpt: Option[String]
12-
1312
def scopedRequirements: Seq[Scoped[BuildRequirements]]
1413
def scopePath: ScopePath
1514
def directivesPositions: Option[Position.File]
15+
def distinctPathOrSource: String = this match {
16+
case PreprocessedSource.OnDisk(p, _, _, _, _, _, _) => p.toString
17+
case PreprocessedSource.InMemory(op, rp, _, _, _, _, _, _, _, _, _) => s"$op; $rp"
18+
case PreprocessedSource.UnwrappedScript(p, _, _, _, _, _, _, _, _, _) => p.toString
19+
case PreprocessedSource.NoSourceCode(_, _, _, _, p) => p.toString
20+
}
1621
}
1722

1823
object PreprocessedSource {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package scala.build.tests
2+
3+
import com.eed3si9n.expecty.Expecty.expect
4+
5+
import scala.build.CollectionOps.distinctBy
6+
class DistinctByTests extends munit.FunSuite {
7+
case class Message(a: String, b: Int)
8+
val distinctData = Seq(
9+
Message(a = "1", b = 4),
10+
Message(a = "2", b = 3),
11+
Message(a = "3", b = 2),
12+
Message(a = "4", b = 1)
13+
)
14+
val repeatingData = Seq(
15+
Message(a = "1", b = 4),
16+
Message(a = "1", b = 44),
17+
Message(a = "2", b = 3),
18+
Message(a = "22", b = 3),
19+
Message(a = "3", b = 22),
20+
Message(a = "33", b = 2),
21+
Message(a = "4", b = 1),
22+
Message(a = "4", b = 11)
23+
)
24+
25+
test("distinctBy where data is already distinct") {
26+
val distinctByA = distinctData.distinctBy(_.a)
27+
val distinctByB = distinctData.distinctBy(_.b)
28+
val generalDistinct = distinctData.distinct
29+
expect(distinctData == generalDistinct)
30+
expect(distinctData == distinctByA)
31+
expect(distinctData == distinctByB)
32+
}
33+
34+
test("distinctBy doesn't change data order") {
35+
val expectedData = Seq(
36+
Message(a = "1", b = 4),
37+
Message(a = "2", b = 3),
38+
Message(a = "22", b = 3),
39+
Message(a = "3", b = 22),
40+
Message(a = "33", b = 2),
41+
Message(a = "4", b = 1)
42+
)
43+
expect(repeatingData.distinctBy(_.a) == expectedData)
44+
}
45+
}

modules/build/src/test/scala/scala/build/tests/SourcesTests.scala

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import coursier.util.{Artifact, Task}
66
import dependency.*
77

88
import scala.build.Ops.*
9-
import scala.build.Sources
9+
import scala.build.{CrossSources, Position, Sources, UnwrappedCrossSources}
1010
import scala.build.internal.ObjectCodeWrapper
11-
import scala.build.CrossSources
12-
import scala.build.Position
1311
import scala.build.errors.{UsingDirectiveValueNumError, UsingDirectiveWrongValueTypeError}
1412
import scala.build.input.ScalaCliInvokeData
1513
import scala.build.options.{BuildOptions, Scope, SuppressWarningOptions}
@@ -536,4 +534,42 @@ class SourcesTests extends munit.FunSuite {
536534
}
537535
}
538536

537+
test("CrossSources.forInputs respects the order of inputs passed") {
538+
val inputArgs @ Seq(project, main, abc, message) =
539+
Seq("project.scala", "Main.scala", "Abc.scala", "Message.scala")
540+
val testInputs = TestInputs(
541+
os.rel / project ->
542+
"""//> using dep "com.lihaoyi::os-lib::0.8.1"
543+
|//> using file "Message.scala"
544+
|""".stripMargin,
545+
os.rel / main ->
546+
"""object Main extends App {
547+
| println(Message(Abc.hello))
548+
|}
549+
|""".stripMargin,
550+
os.rel / abc ->
551+
"""object Abc {
552+
| val hello = "Hello"
553+
|}
554+
|""".stripMargin,
555+
os.rel / message ->
556+
"""case class Message(value: String)
557+
|""".stripMargin
558+
)
559+
testInputs.withInputs { (_, inputs) =>
560+
val crossSourcesResult =
561+
CrossSources.forInputs(
562+
inputs,
563+
preprocessors,
564+
TestLogger(),
565+
SuppressWarningOptions()
566+
)
567+
assert(crossSourcesResult.isRight)
568+
val Right(CrossSources(onDiskSources, _, _, _, _)) =
569+
crossSourcesResult.map(_._1.withWrappedScripts(BuildOptions()))
570+
val onDiskPaths = onDiskSources.map(_.value._1.last)
571+
expect(onDiskPaths == inputArgs)
572+
}
573+
}
574+
539575
}

modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ object Export extends ScalaCommand[ExportOptions] {
4040

4141
logger.log("Preparing build")
4242

43-
val (crossSources, _) = value {
43+
val (crossSources: UnwrappedCrossSources, _) = value {
4444
CrossSources.forInputs(
4545
inputs,
4646
Sources.defaultPreprocessors(
@@ -56,8 +56,9 @@ object Export extends ScalaCommand[ExportOptions] {
5656

5757
val wrappedScriptsSources = crossSources.withWrappedScripts(buildOptions)
5858

59-
val scopedSources = value(wrappedScriptsSources.scopedSources(buildOptions))
60-
val sources = scopedSources.sources(scope, wrappedScriptsSources.sharedOptions(buildOptions))
59+
val scopedSources: ScopedSources = value(wrappedScriptsSources.scopedSources(buildOptions))
60+
val sources: Sources =
61+
scopedSources.sources(scope, wrappedScriptsSources.sharedOptions(buildOptions))
6162

6263
if (verbosity >= 3)
6364
pprint.err.log(sources)

modules/integration/src/test/scala/scala/cli/integration/ExportCommonTestDefinitions.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ trait ExportCommonTestDefinitions { _: ScalaCliSuite & TestScalaVersionArgs =>
6161
expect(output.contains("Hello"))
6262
}
6363

64+
def extraSourceFromDirectiveWithExtraDependency(inputs: String*): Unit =
65+
prepareTestInputs(
66+
ExportTestProjects.extraSourceFromDirectiveWithExtraDependency(actualScalaVersion)
67+
).fromRoot { root =>
68+
exportCommand(inputs*).call(cwd = root, stdout = os.Inherit)
69+
val res = buildToolCommand(root, runMainArgs*)
70+
.call(cwd = root / outputDir)
71+
val output = res.out.trim(Charset.defaultCharset())
72+
expect(output.contains(root.toString))
73+
}
74+
6475
if (runExportTests) {
6576
test("JVM") {
6677
jvmTest()
@@ -74,5 +85,11 @@ trait ExportCommonTestDefinitions { _: ScalaCliSuite & TestScalaVersionArgs =>
7485
test("Ensure test framework NPE is not thrown when depending on logback") {
7586
logbackBugCase()
7687
}
88+
test("extra source from a directive introducing a dependency") {
89+
extraSourceFromDirectiveWithExtraDependency("Main.scala")
90+
}
91+
test("extra source passed both via directive and from command line") {
92+
extraSourceFromDirectiveWithExtraDependency(".")
93+
}
7794
}
7895
}

modules/integration/src/test/scala/scala/cli/integration/ExportTestProjects.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,19 @@ object ExportTestProjects {
279279
|//> using lib "ch.qos.logback:logback-classic:1.4.5"
280280
|println("Hello")
281281
|""".stripMargin)
282+
283+
def extraSourceFromDirectiveWithExtraDependency(scalaVersion: String): TestInputs =
284+
TestInputs(
285+
os.rel / "Main.scala" ->
286+
s"""//> using scala "$scalaVersion"
287+
|//> using file "Message.scala"
288+
|object Main extends App {
289+
| println(Message(value = os.pwd.toString).value)
290+
|}
291+
|""".stripMargin,
292+
os.rel / "Message.scala" ->
293+
s"""//> using dep "com.lihaoyi::os-lib:0.9.1"
294+
|case class Message(value: String)
295+
|""".stripMargin
296+
)
282297
}

0 commit comments

Comments
 (0)