@@ -68,9 +68,16 @@ object BuildGen {
6868 testParallelism = parentValue(a.testParallelism, b.testParallelism),
6969 testSandboxWorkingDir = parentValue(a.testSandboxWorkingDir, b.testSandboxWorkingDir)
7070 )
71+ // For test modules, don't extract common mvnDeps because YAML can't append to parent deps
72+ def parentTestModule (a : ModuleSpec , b : ModuleSpec ) = parentModule0(a, b, Seq (testSupertype)).copy(
73+ mandatoryMvnDeps = Values (),
74+ mvnDeps = Values (),
75+ compileMvnDeps = Values (),
76+ runMvnDeps = Values ()
77+ )
7178 def parentModule (a : ModuleSpec , b : ModuleSpec ) =
7279 parentModule0(a, b, moduleHierarchy.take(1 )).copy(
73- test = (a.test.toSeq ++ b.test.toSeq).reduceOption(parentModule0(_, _, Seq (testSupertype)) )
80+ test = (a.test.toSeq ++ b.test.toSeq).reduceOption(parentTestModule )
7481 )
7582 def extendValue [A ](a : Value [A ], parent : Value [A ]) = a.copy(
7683 if (a.base == parent.base) None else a.base,
@@ -86,7 +93,15 @@ object BuildGen {
8693 a.appendSuper || parent.base.nonEmpty || parent.cross.nonEmpty
8794 )
8895 def extendModule0 (a : ModuleSpec , parent : ModuleSpec ): ModuleSpec = a.copy(
89- supertypes = (parent.name +: a.supertypes).diff(parent.supertypes),
96+ // Don't subtract nested test traits (MavenTests, SbtTests, etc.) - they can't be inherited
97+ // through a standalone trait and must remain in the child's extends clause.
98+ // Put nested traits first for proper linearization (MavenTests before ProjectBaseModuleTests)
99+ supertypes = {
100+ val filtered = (a.supertypes :+ parent.name).diff(parent.supertypes.filterNot(nestedTestTraits.contains))
101+ // Reorder: nested test traits first, then others
102+ val (nested, others) = filtered.partition(nestedTestTraits.contains)
103+ nested ++ others
104+ },
90105 mixins = if (a.mixins == parent.mixins) Nil else a.mixins,
91106 repositories = extendValues(a.repositories, parent.repositories),
92107 forkArgs = extendValues(a.forkArgs, parent.forkArgs),
@@ -134,11 +149,17 @@ object BuildGen {
134149 Option .when(childModules.length > 1 ) {
135150 val baseModule = childModules.reduce(parentModule)
136151 val baseModule0 = baseModule.copy(
137- name = " ProjectBaseModule" ,
138- test = baseModule.test.map(_.copy(name = " Tests" ))
152+ name = " millbuild.ProjectBaseModule" ,
153+ // Use a top-level Tests trait instead of nested one for YAML compatibility
154+ test = baseModule.test.map(_.copy(name = " millbuild.ProjectBaseModuleTests" ))
139155 )
140156 val packages0 = packages.map(pkg => pkg.copy(module = extendModule(pkg.module, baseModule0)))
141- (baseModule0, packages0)
157+ // Return the base module with simple name for file generation
158+ val baseModuleForFile = baseModule0.copy(
159+ name = " ProjectBaseModule" ,
160+ test = baseModule0.test.map(_.copy(name = " ProjectBaseModuleTests" ))
161+ )
162+ (baseModuleForFile, packages0)
142163 }
143164 }
144165
@@ -185,6 +206,17 @@ object BuildGen {
185206 val file = os.sub / os.SubPath (s " mill-build/src/ ${module.name}.scala " )
186207 println(s " writing $file" )
187208 os.write(os.pwd / file, renderBaseModule(module), createFolders = true )
209+ // Also write a build.mill with "See Also" directive to trigger Mill to include mill-build/src/
210+ val buildMillFile = os.pwd / " build.mill"
211+ if (! os.exists(buildMillFile)) {
212+ println(" writing build.mill" )
213+ os.write(
214+ buildMillFile,
215+ s """ |/** See Also: build.mill.yaml */
216+ |/** See Also: mill-build/src/ ${module.name}.scala */
217+ | """ .stripMargin
218+ )
219+ }
188220 }
189221
190222 val rootPackage +: nestedPackages = packages0 : @ unchecked
@@ -195,13 +227,13 @@ object BuildGen {
195227 os.pwd / " build.mill.yaml" ,
196228 s """ mill-version: SNAPSHOT
197229 |mill-jvm-version: $millJvmVersion
198- | $millJvmOptsLine${renderYamlPackage(rootPackage)}
230+ | $millJvmOptsLine${renderYamlPackage(rootPackage, isRootBuild = true )}
199231 | """ .stripMargin
200232 )
201233 for (pkg <- nestedPackages) do {
202234 val file = os.sub / pkg.dir / " package.mill.yaml"
203235 println(s " writing $file" )
204- os.write(os.pwd / file, renderYamlPackage(pkg))
236+ os.write(os.pwd / file, renderYamlPackage(pkg, isRootBuild = false ))
205237 }
206238 }
207239
@@ -234,16 +266,29 @@ object BuildGen {
234266 // Scala rendering for ProjectBaseModule.scala
235267 // ============================================
236268
269+ // These are nested traits that can't be extended by standalone traits
270+ private val nestedTestTraits = Set (" MavenTests" , " SbtTests" , " JavaTests" , " ScalaTests" )
271+
237272 private def renderBaseModule (module : ModuleSpec ) = {
238273 import module .*
239- s """ package millbuild
240- | ${renderScalaImports(module)}
241- |trait $name ${renderScalaExtendsClause(supertypes ++ mixins)} {
274+ val mainTrait = s """ trait $name ${renderScalaExtendsClause(supertypes ++ mixins)} {
242275 |
243276 | ${renderScalaModuleBody(module)}
244- |
245- | ${test.fold(" " )(renderScalaTestModule(" trait" , _))}
246277 |} """ .stripMargin
278+ // Generate Tests as a top-level trait for YAML compatibility
279+ // Filter out nested test traits (like MavenTests) that can't be extended by standalone traits
280+ val testTrait = test.fold(" " ) { testSpec =>
281+ val standaloneSupertypes = testSpec.supertypes.filterNot(nestedTestTraits.contains)
282+ s """
283+ |
284+ |trait ${testSpec.name} ${renderScalaExtendsClause(standaloneSupertypes ++ testSpec.mixins)} {
285+ |
286+ | ${renderScalaModuleBody(testSpec)}
287+ |} """ .stripMargin
288+ }
289+ s """ package millbuild
290+ | ${renderScalaImports(module)}
291+ | $mainTrait$testTrait""" .stripMargin
247292 }
248293
249294 private def renderScalaImports (module : ModuleSpec ) = {
@@ -370,7 +415,7 @@ object BuildGen {
370415 collection : String = " Seq"
371416 ) = {
372417 val defType = if (isTask) " def" else " override def"
373- val appender = if (values.appendSuper) s " ++ super. $name" else " "
418+ val appender = if (values.appendSuper) s " ++ super. $name() " else " "
374419 val cross = values.cross.map { (key, values) =>
375420 s """ case " $key" => $collection( ${values.map(encode).mkString(" , " )}) $appender"""
376421 }.mkString(lineSeparator)
@@ -431,7 +476,7 @@ object BuildGen {
431476
432477 private def encodeScalaString (s : String ) = literalize(s)
433478
434- private def encodeScalaOpt (opt : Opt ) = opt.group.map(s => literalize(s)).mkString(" Seq( " , " , " , " ) " )
479+ private def encodeScalaOpt (opt : Opt ) = opt.group.map(s => literalize(s)).mkString(" , " )
435480
436481 private def encodeScalaLiteralOpt (opt : Opt ) = opt.group.mkString(" Seq(" , " , " , " )" )
437482
@@ -493,87 +538,105 @@ object BuildGen {
493538 // YAML rendering for build.mill.yaml and package.mill.yaml
494539 // ============================================
495540
496- private def renderYamlPackage (pkg : PackageSpec ): String = {
497- renderYamlModule(pkg.module, indent = 0 , isRoot = true )
541+ private def renderYamlPackage (pkg : PackageSpec , isRootBuild : Boolean ): String = {
542+ renderYamlModule(pkg.module, indent = 0 , isRoot = isRootBuild )
498543 }
499544
500545 private def renderYamlModule (module : ModuleSpec , indent : Int , isRoot : Boolean ): String = {
501546 val sb = new StringBuilder
502547 val prefix = " " * indent
503548
504- // Render extends
505- val allSupertypes = module.supertypes ++ module.mixins
506- if (allSupertypes.nonEmpty) {
507- if (allSupertypes.size == 1 ) {
508- sb ++= s " ${prefix}extends: ${allSupertypes.head}$lineSeparator"
509- } else {
510- sb ++= s " ${prefix}extends: [ ${allSupertypes.mkString(" , " )}] $lineSeparator"
549+ // Render extends (not for root module - root modules don't extend regular module traits)
550+ if (! isRoot) {
551+ val allSupertypes = module.supertypes ++ module.mixins
552+ if (allSupertypes.nonEmpty) {
553+ if (allSupertypes.size == 1 ) {
554+ sb ++= s " ${prefix}extends: ${allSupertypes.head}$lineSeparator"
555+ } else {
556+ sb ++= s " ${prefix}extends: [ ${allSupertypes.mkString(" , " )}] $lineSeparator"
557+ }
511558 }
512559 }
513560
514- // Render module deps
515- renderYamlModuleDeps(sb, prefix, " moduleDeps" , module.moduleDeps)
516- renderYamlModuleDeps(sb, prefix, " compileModuleDeps" , module.compileModuleDeps)
517- renderYamlModuleDeps(sb, prefix, " runModuleDeps" , module.runModuleDeps)
518- renderYamlModuleDeps(sb, prefix, " bomModuleDeps" , module.bomModuleDeps)
519-
520- // Render maven deps
521- renderYamlMvnDeps(
522- sb,
523- prefix,
524- " mvnDeps" ,
525- combineMvnDeps(module.mandatoryMvnDeps, module.mvnDeps)
526- )
527- renderYamlMvnDeps(sb, prefix, " compileMvnDeps" , module.compileMvnDeps)
528- renderYamlMvnDeps(sb, prefix, " runMvnDeps" , module.runMvnDeps)
529- renderYamlMvnDeps(sb, prefix, " bomMvnDeps" , module.bomMvnDeps)
530- renderYamlMvnDeps(sb, prefix, " depManagement" , module.depManagement)
531- renderYamlMvnDeps(sb, prefix, " scalacPluginMvnDeps" , module.scalacPluginMvnDeps)
532-
533- // Render scala/java configuration
534- renderYamlValue(sb, prefix, " scalaVersion" , module.scalaVersion)
535- renderYamlValues(sb, prefix, " scalacOptions" , module.scalacOptions, encodeYamlOpt)
536- renderYamlValues(sb, prefix, " javacOptions" , module.javacOptions, encodeYamlOpt)
537- renderYamlValue(sb, prefix, " scalaJSVersion" , module.scalaJSVersion)
538- renderYamlValue(sb, prefix, " moduleKind" , module.moduleKind)
539- renderYamlValue(sb, prefix, " scalaNativeVersion" , module.scalaNativeVersion)
540-
541- // Render sources and resources
542- renderYamlPaths(sb, prefix, " sources" , module.sources)
543- renderYamlPaths(sb, prefix, " resources" , module.resources)
544- renderYamlSubPaths(sb, prefix, " sourcesRootFolders" , module.sourcesRootFolders)
545- renderYamlSubPaths(sb, prefix, " sourcesFolders" , module.sourcesFolders)
546-
547- // Render fork configuration
548- renderYamlValues(sb, prefix, " forkArgs" , module.forkArgs, encodeYamlOpt)
549- renderYamlForkWorkingDir(sb, prefix, module.forkWorkingDir)
550-
551- // Render repositories
552- renderYamlStrings(sb, prefix, " repositories" , module.repositories)
553-
554- // Render error prone configuration
555- renderYamlMvnDeps(sb, prefix, " errorProneDeps" , module.errorProneDeps)
556- renderYamlStrings(sb, prefix, " errorProneOptions" , module.errorProneOptions)
557- renderYamlValues(
558- sb,
559- prefix,
560- " errorProneJavacEnableOptions" ,
561- module.errorProneJavacEnableOptions,
562- encodeYamlOpt
563- )
561+ // Module-specific configuration (not for root module - it's not a JavaModule)
562+ if (! isRoot) {
563+ // Render module deps
564+ renderYamlModuleDeps(sb, prefix, " moduleDeps" , module.moduleDeps)
565+ renderYamlModuleDeps(sb, prefix, " compileModuleDeps" , module.compileModuleDeps)
566+ renderYamlModuleDeps(sb, prefix, " runModuleDeps" , module.runModuleDeps)
567+ renderYamlModuleDeps(sb, prefix, " bomModuleDeps" , module.bomModuleDeps)
568+
569+ // Render maven deps
570+ renderYamlMvnDeps(
571+ sb,
572+ prefix,
573+ " mvnDeps" ,
574+ combineMvnDeps(module.mandatoryMvnDeps, module.mvnDeps)
575+ )
576+ renderYamlMvnDeps(sb, prefix, " compileMvnDeps" , module.compileMvnDeps)
577+ renderYamlMvnDeps(sb, prefix, " runMvnDeps" , module.runMvnDeps)
578+ renderYamlMvnDeps(sb, prefix, " bomMvnDeps" , module.bomMvnDeps)
579+ renderYamlMvnDeps(sb, prefix, " depManagement" , module.depManagement)
580+ renderYamlMvnDeps(sb, prefix, " scalacPluginMvnDeps" , module.scalacPluginMvnDeps)
581+
582+ // Render scala/java configuration
583+ renderYamlValue(sb, prefix, " scalaVersion" , module.scalaVersion)
584+ renderYamlValues(sb, prefix, " scalacOptions" , module.scalacOptions, encodeYamlOpt)
585+ renderYamlValues(sb, prefix, " javacOptions" , module.javacOptions, encodeYamlOpt)
586+ renderYamlValue(sb, prefix, " scalaJSVersion" , module.scalaJSVersion)
587+ renderYamlValue(sb, prefix, " moduleKind" , module.moduleKind)
588+ renderYamlValue(sb, prefix, " scalaNativeVersion" , module.scalaNativeVersion)
589+
590+ // Render sources and resources
591+ renderYamlPaths(sb, prefix, " sources" , module.sources)
592+ renderYamlPaths(sb, prefix, " resources" , module.resources)
593+ renderYamlSubPaths(sb, prefix, " sourcesRootFolders" , module.sourcesRootFolders)
594+ renderYamlSubPaths(sb, prefix, " sourcesFolders" , module.sourcesFolders)
595+ }
564596
565- // Render publishing configuration
566- renderYamlValue(sb, prefix, " artifactName" , module.artifactName)
567- renderYamlValue(sb, prefix, " pomPackagingType" , module.pomPackagingType)
568- renderYamlPomParentProject(sb, prefix, module.pomParentProject)
569- renderYamlPomSettings(sb, prefix, module.pomSettings)
570- renderYamlValue(sb, prefix, " publishVersion" , module.publishVersion)
571- renderYamlVersionScheme(sb, prefix, module.versionScheme)
572- renderYamlPublishProperties(sb, prefix, module.publishProperties)
573-
574- // Render test configuration
575- renderYamlBoolValue(sb, prefix, " testParallelism" , module.testParallelism)
576- renderYamlBoolValue(sb, prefix, " testSandboxWorkingDir" , module.testSandboxWorkingDir)
597+ // Render additional module-specific configuration (not for root module)
598+ if (! isRoot) {
599+ // Render fork configuration
600+ renderYamlValues(sb, prefix, " forkArgs" , module.forkArgs, encodeYamlOpt)
601+ renderYamlForkWorkingDir(sb, prefix, module.forkWorkingDir)
602+
603+ // Render repositories
604+ renderYamlStrings(sb, prefix, " repositories" , module.repositories)
605+
606+ // Render error prone configuration
607+ renderYamlMvnDeps(sb, prefix, " errorProneDeps" , module.errorProneDeps)
608+ renderYamlStrings(sb, prefix, " errorProneOptions" , module.errorProneOptions)
609+ renderYamlValues(
610+ sb,
611+ prefix,
612+ " errorProneJavacEnableOptions" ,
613+ module.errorProneJavacEnableOptions,
614+ encodeYamlOpt
615+ )
616+
617+ // Render publishing configuration only for modules that extend PublishModule
618+ val extendsPublishModule = module.supertypes.exists(s =>
619+ s.contains(" PublishModule" ) || s.contains(" ProjectBaseModule" )
620+ )
621+ if (extendsPublishModule) {
622+ renderYamlValue(sb, prefix, " artifactName" , module.artifactName)
623+ // Skip pomPackagingType for pom/war - these are aggregator/web modules in Maven
624+ // that don't need special packaging in Mill
625+ val skipPackagingType = module.pomPackagingType.base.exists(p => p == " pom" || p == " war" )
626+ if (! skipPackagingType) {
627+ renderYamlValue(sb, prefix, " pomPackagingType" , module.pomPackagingType)
628+ }
629+ renderYamlPomParentProject(sb, prefix, module.pomParentProject)
630+ renderYamlPomSettings(sb, prefix, module.pomSettings)
631+ renderYamlValue(sb, prefix, " publishVersion" , module.publishVersion)
632+ renderYamlVersionScheme(sb, prefix, module.versionScheme)
633+ renderYamlPublishProperties(sb, prefix, module.publishProperties)
634+ }
635+
636+ // Render test configuration
637+ renderYamlBoolValue(sb, prefix, " testParallelism" , module.testParallelism)
638+ renderYamlBoolValue(sb, prefix, " testSandboxWorkingDir" , module.testSandboxWorkingDir)
639+ }
577640
578641 // Render test submodule
579642 module.test.foreach { testSpec =>
0 commit comments