diff --git a/.scalafmt.conf b/.scalafmt.conf index 07cbc446c4..cedf57993b 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.0.0" +version = "3.0.3" align.preset = more maxColumn = 100 @@ -12,6 +12,8 @@ newlines.beforeMultiline = keep newlines.afterCurlyLambdaParams = keep newlines.alwaysBeforeElseAfterCurlyIf = true +runner.dialect = scala213 + fileOverride { "glob:**/scala-3-stable/**" { runner.dialect = scala3 diff --git a/build.sc b/build.sc index 19d3b87d0e..a788f7ba5a 100644 --- a/build.sc +++ b/build.sc @@ -100,9 +100,9 @@ object `generate-reference-doc` extends SbtModule { } object dummy extends Module { - // dummy project to get scala steward updates for Ammonite, whose - // version is used in the repl command, and ensure Ammonite is available - // for all Scala versions we support + // dummy projects to get scala steward updates for Ammonite and scalafmt, whose + // versions are used in the fmt and repl commands, and ensure Ammonite is available + // for all Scala versions we support. object amm extends Cross[Amm](Scala.listAll: _*) class Amm(val crossScalaVersion: String) extends CrossScalaModule with Bloop.Module { def skipBloop = true @@ -110,6 +110,13 @@ object dummy extends Module { Deps.ammonite ) } + object scalafmt extends ScalaModule with Bloop.Module { + def skipBloop = true + def scalaVersion = Scala.defaultInternal + def ivyDeps = Agg( + Deps.scalafmtCli + ) + } } class BuildMacros(val crossScalaVersion: String) extends CrossSbtModule with ScalaCliPublishModule { @@ -134,6 +141,9 @@ class Build(val crossScalaVersion: String) def repositories = super.repositories ++ Seq( coursier.Repositories.sonatype("snapshots") ) + def compileIvyDeps = super.compileIvyDeps() ++ Agg( + Deps.svm + ) def ivyDeps = super.ivyDeps() ++ Agg( Deps.asm, Deps.bloopConfig, @@ -218,6 +228,8 @@ class Build(val crossScalaVersion: String) | | def ammoniteVersion = "${Deps.ammonite.dep.version}" | + | def defaultScalafmtVersion = "${Deps.scalafmtCli.dep.version}" + | | def defaultScalaVersion = "${Scala.defaultUser}" |} |""".stripMargin diff --git a/examples/cross-build/Hello.js.scala b/examples/cross-build/Hello.js.scala new file mode 100644 index 0000000000..fbf406d40e --- /dev/null +++ b/examples/cross-build/Hello.js.scala @@ -0,0 +1,11 @@ +// using scala 2.13 +// require scala-js + +import scala.scalajs.js + +object Test { + def main(args: Array[String]): Unit = { + val console = js.Dynamic.global.console + console.log("Hello from Scala.JS") + } +} diff --git a/examples/cross-build/Hello.native.scala b/examples/cross-build/Hello.native.scala new file mode 100644 index 0000000000..2c99ac11ae --- /dev/null +++ b/examples/cross-build/Hello.native.scala @@ -0,0 +1,14 @@ +// using scala 2.13 +// require scala-native + +import scala.scalanative.libc._ +import scala.scalanative.unsafe._ + +object Test { + def main(args: Array[String]): Unit = { + val message = "Hello from Scala Native\n" + Zone { implicit z => + stdio.printf(toCString(message)) + } + } +} diff --git a/examples/cross-build/Hello.scala b/examples/cross-build/Hello.scala deleted file mode 100644 index 3684175d96..0000000000 --- a/examples/cross-build/Hello.scala +++ /dev/null @@ -1,6 +0,0 @@ -// using scala "2.12.14", "2.13.6", "3.0.1" - -object Hello { - def main(args: Array[String]): Unit = - println("Hello") -} diff --git a/modules/build/src/main/java/scala/build/internal/Chdir.java b/modules/build/src/main/java/scala/build/internal/Chdir.java new file mode 100644 index 0000000000..f73f894bb0 --- /dev/null +++ b/modules/build/src/main/java/scala/build/internal/Chdir.java @@ -0,0 +1,15 @@ +package scala.build.internal; + +import coursier.jvm.ErrnoException; + +public final class Chdir { + + public static boolean available() { + return false; + } + + public static void chdir(String path) throws ErrnoException { + // Not supported on the JVM, returning immediately + } + +} \ No newline at end of file diff --git a/modules/build/src/main/java/scala/build/internal/ChdirGraalvm.java b/modules/build/src/main/java/scala/build/internal/ChdirGraalvm.java new file mode 100644 index 0000000000..e551d583e3 --- /dev/null +++ b/modules/build/src/main/java/scala/build/internal/ChdirGraalvm.java @@ -0,0 +1,37 @@ +package scala.build.internal; + +import java.io.FileNotFoundException; + +import com.oracle.svm.core.CErrorNumber; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import coursier.jvm.ErrnoException; +import coursier.jvm.GraalvmErrnoExtras; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +@TargetClass(className = "scala.build.internal.Chdir") +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +final class ChdirGraalvm { + + @Substitute + public static boolean available() { + return true; + } + + @Substitute + public static void chdir(String path) throws ErrnoException { + CTypeConversion.CCharPointerHolder path0 = CTypeConversion.toCString(path); + int ret = GraalvmUnistdExtras.chdir(path0.get()); + + if (ret != 0) { + int n = CErrorNumber.getCErrorNumber(); + Throwable cause = null; + if (n == GraalvmErrnoExtras.ENOENT() || n == GraalvmErrnoExtras.ENOTDIR()) + cause = new FileNotFoundException(path); + throw new ErrnoException(n, cause); + } + } + +} \ No newline at end of file diff --git a/modules/build/src/main/java/scala/build/internal/GraalvmUnistdExtras.java b/modules/build/src/main/java/scala/build/internal/GraalvmUnistdExtras.java new file mode 100644 index 0000000000..ef8d9b65b6 --- /dev/null +++ b/modules/build/src/main/java/scala/build/internal/GraalvmUnistdExtras.java @@ -0,0 +1,18 @@ +package scala.build.internal; + +import com.oracle.svm.core.posix.headers.PosixDirectives; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CCharPointerPointer; + +@CContext(PosixDirectives.class) +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +public class GraalvmUnistdExtras { + + @CFunction + public static native int chdir(CCharPointer path); + +} diff --git a/modules/build/src/main/scala/scala/build/internal/OsLibc.scala b/modules/build/src/main/scala/scala/build/internal/OsLibc.scala index c2fc84db69..50ff992415 100644 --- a/modules/build/src/main/scala/scala/build/internal/OsLibc.scala +++ b/modules/build/src/main/scala/scala/build/internal/OsLibc.scala @@ -9,7 +9,7 @@ import scala.util.Properties object OsLibc { - private lazy val isMusl: Option[Boolean] = { + lazy val isMusl: Option[Boolean] = { def tryRun(cmd: String*): Option[os.CommandResult] = try { diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index bef941daa7..7c46b7f6d1 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -18,7 +18,8 @@ object Runner { commandName: String, command: Seq[String], logger: Logger, - allowExecve: Boolean = false + allowExecve: Boolean = false, + cwd: Option[os.Path] = None ): Int = { import logger.{log, debug} @@ -31,6 +32,10 @@ object Runner { if (allowExecve && Execve.available()) { debug("execve available") + + for (dir <- cwd) + Chdir.chdir(dir.toString) + Execve.execve( findInPath(command.head).fold(command.head)(_.toString), commandName +: command.tail.toArray, @@ -38,11 +43,13 @@ object Runner { ) sys.error("should not happen") } - else - new ProcessBuilder(command: _*) + else { + val b = new ProcessBuilder(command: _*) .inheritIO() - .start() - .waitFor() + for (dir <- cwd) + b.directory(dir.toIO) + b.start().waitFor() + } } def runJvm( diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala index 2e56d1c43e..cca63190a3 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala @@ -21,6 +21,7 @@ object ScalaCli extends CommandsEntryPoint { Compile, Directories, Export, + Fmt, InstallCompletions, Metabrowse, Repl, diff --git a/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala b/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala new file mode 100644 index 0000000000..d3dfce7251 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala @@ -0,0 +1,54 @@ +package scala.cli.commands + +import caseapp._ + +import scala.build.Inputs +import scala.build.internal.Runner +import scala.cli.internal.FetchExternalBinary + +object Fmt extends ScalaCommand[FmtOptions] { + override def group = "Miscellaneous" + override def sharedOptions(options: FmtOptions) = Some(options.shared) + def run(options: FmtOptions, args: RemainingArgs): Unit = { + + // TODO If no input is given, just pass '.' to scalafmt? + val (sourceFiles, workspace) = + if (args.remaining.isEmpty) + (Seq(os.pwd), os.pwd) + else { + val i = options.shared.inputsOrExit(args) + val s = i.sourceFiles().collect { + case sc: Inputs.Script => sc.path + case sc: Inputs.ScalaFile => sc.path + } + (s, i.workspace) + } + + val logger = options.shared.logger + val cache = options.shared.coursierCache + + if (sourceFiles.isEmpty) + logger.debug("No source files, not formatting anything") + else { + + val fmtLauncher = options.scalafmtLauncher.filter(_.nonEmpty) match { + case Some(launcher) => + os.Path(launcher, os.pwd) + case None => + val (url, changing) = options.binaryUrl + FetchExternalBinary.fetch(url, changing, cache, logger, "scalafmt") + } + + logger.debug(s"Using scalafmt launcher $fmtLauncher") + + val command = Seq(fmtLauncher.toString) ++ sourceFiles.map(_.toString) + Runner.run( + "scalafmt", + command, + logger, + allowExecve = true, + cwd = Some(workspace) + ) + } + } +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala new file mode 100644 index 0000000000..cb3617c565 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala @@ -0,0 +1,41 @@ +package scala.cli.commands + +import caseapp._ + +import scala.build.internal.Constants +import scala.cli.internal.FetchExternalBinary +import scala.util.Properties + +// format: off +@HelpMessage("Format Scala code") +final case class FmtOptions( + @Recurse + shared: SharedOptions = SharedOptions(), + @HelpMessage("Check that sources are well formatted") + check: Boolean = false, + + @Hidden + osArchSuffix: Option[String] = None, + @Hidden + scalafmtTag: Option[String] = None, + @Hidden + scalafmtGitHubOrgName: Option[String] = None, + @Hidden + scalafmtExtension: Option[String] = None, + @Hidden + scalafmtLauncher: Option[String] = None +) { + // format: on + + def binaryUrl: (String, Boolean) = { + val osArchSuffix0 = osArchSuffix.map(_.trim).filter(_.nonEmpty) + .getOrElse(FetchExternalBinary.platformSuffix()) + val tag0 = scalafmtTag.getOrElse("v" + Constants.defaultScalafmtVersion) + val gitHubOrgName0 = scalafmtGitHubOrgName.getOrElse("alexarchambault/scalafmt-native-image") + val extension0 = if (Properties.isWin) ".zip" else ".gz" + val url = + s"https://github.com/$gitHubOrgName0/releases/download/$tag0/scalafmt-$osArchSuffix0$extension0" + (url, !tag0.startsWith("v")) + } + +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala b/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala index a886b9e763..e1621ddbf7 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala @@ -13,6 +13,7 @@ import coursier.util.{Artifact, Task} import scala.build.{Build, BuildThreads, Inputs, Logger, Os} import scala.build.internal.Runner import scala.build.options.BuildOptions +import scala.cli.internal.FetchExternalBinary import scala.concurrent.ExecutionContext.{global => ec} import scala.util.Properties @@ -59,104 +60,23 @@ object Metabrowse extends ScalaCommand[MetabrowseOptions] { sourceJar: Path ): Unit = { - def defaultLauncher() = { - - val (url, changing) = - options.metabrowseBinaryUrl(successfulBuild.options.scalaParams.scalaVersion) - val cache = options.shared.coursierCache - val f = cache.logger.use { - logger.log(s"Getting $url") - cache.file(Artifact(url).withChanging(changing)).run.flatMap { - case Left(e) => Task.fail(e) - case Right(f) => Task.point(os.Path(f, os.pwd)) - }.unsafeRun()(cache.ec) - } - logger.debug(s"$url is available locally at $f") - - // FIXME Once coursier has proper support for extracted archives in cache, use it instead of those hacks - if (f.last.endsWith(".zip")) { - val baseDir = f / os.up - val dir = baseDir / s".${f.last.stripSuffix(".zip")}-content" - if (os.exists(dir)) - logger.debug(s"Found $dir") - else { - logger.debug(s"Unzipping $f under $dir") - val tmpDir = baseDir / s".${f.last.stripSuffix(".zip")}-content-${UUID.randomUUID()}" - try { - coursier.jvm.UnArchiver.default().extract( - ArchiveType.Zip, - f.toIO, - tmpDir.toIO, - overwrite = false - ) - if (!os.exists(dir)) { - try os.move(tmpDir, dir, atomicMove = true) - catch { - case ex: IOException => - if (!os.exists(dir)) - throw new Exception(ex) - } - } - } - finally { - try os.remove.all(tmpDir) - catch { - case _: IOException if Properties.isWin => - } - } - } - - val dirContent = os.list(dir) - if (dirContent.length == 1) dirContent.head - else dirContent.filter(_.last.startsWith("metabrowse")).head - } - else if (f.last.endsWith(".gz")) { - val dest = f / os.up / s".${f.last.stripSuffix(".gz")}" - if (os.exists(dest)) - logger.debug(s"Found $dest") - else { - logger.debug(s"Uncompression $f at $dest") - var fis: FileInputStream = null - var fos: FileOutputStream = null - var gzis: GZIPInputStream = null - try { - fis = new FileInputStream(f.toIO) - gzis = new GZIPInputStream(fis) - fos = new FileOutputStream(dest.toIO) - - val buf = Array.ofDim[Byte](16 * 1024) - var read = -1 - while ({ - read = gzis.read(buf) - read >= 0 - }) { - if (read > 0) - fos.write(buf, 0, read) - } - fos.flush() - } - finally { - if (gzis != null) gzis.close() - if (fos != null) fos.close() - if (fis != null) fis.close() - } - } - dest - } - else - f - } - val launcher = options.metabrowseLauncher .filter(_.nonEmpty) .map(os.Path(_, os.pwd)) - .getOrElse(defaultLauncher()) + .getOrElse { + val (url, changing) = + options.metabrowseBinaryUrl(successfulBuild.options.scalaParams.scalaVersion) + FetchExternalBinary.fetch( + url, + changing, + options.shared.coursierCache, + logger, + "metabrowse" + ) + } logger.debug(s"Using metabrowse launcher $launcher") - if (!Properties.isWin) - os.perms.set(launcher, "rwxr-xr-x") - val extraJars = if (options.addRtJar.getOrElse(true)) { diff --git a/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala index 0f1c765ca3..418ce66dfc 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala @@ -3,6 +3,7 @@ package scala.cli.commands import caseapp._ import scala.build.options.BuildOptions +import scala.cli.internal.FetchExternalBinary import scala.util.Properties // format: off @@ -40,7 +41,7 @@ final case class MetabrowseOptions( def metabrowseBinaryUrl(scalaVersion: String): (String, Boolean) = { val osArchSuffix0 = osArchSuffix.map(_.trim).filter(_.nonEmpty) - .getOrElse(MetabrowseOptions.platformSuffix) + .getOrElse(FetchExternalBinary.platformSuffix(supportsMusl = false)) val metabrowseTag0 = metabrowseTag.getOrElse("latest") val metabrowseGitHubOrgName0 = metabrowseGitHubOrgName.getOrElse("alexarchambault/metabrowse") val metabrowseExtension0 = if (Properties.isWin) ".zip" else ".gz" @@ -63,20 +64,6 @@ final case class MetabrowseOptions( } object MetabrowseOptions { - - private def platformSuffix: String = { - val arch = sys.props("os.arch").toLowerCase(java.util.Locale.ROOT) match { - case "amd64" => "x86_64" - case other => other - } - val os = - if (Properties.isWin) "pc-win32" - else if (Properties.isLinux) "pc-linux" - else if (Properties.isMac) "apple-darwin" - else sys.error(s"Unrecognized OS: ${sys.props("os.name")}") - s"$arch-$os" - } - implicit val parser = Parser[MetabrowseOptions] implicit val help = Help[MetabrowseOptions] } diff --git a/modules/cli/src/main/scala/scala/cli/internal/FetchExternalBinary.scala b/modules/cli/src/main/scala/scala/cli/internal/FetchExternalBinary.scala new file mode 100644 index 0000000000..6117b56ec0 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/internal/FetchExternalBinary.scala @@ -0,0 +1,132 @@ +package scala.cli.internal + +import coursier.cache.FileCache +import coursier.jvm.ArchiveType +import coursier.util.{Artifact, Task} + +import java.io.{FileInputStream, FileOutputStream, IOException} +import java.util.{Locale, UUID} +import java.util.zip.GZIPInputStream + +import scala.build.internal.OsLibc +import scala.build.Logger +import scala.util.Properties + +object FetchExternalBinary { + + def fetch( + url: String, + changing: Boolean, + cache: FileCache[Task], + logger: Logger, + launcherPrefix: String + ) = { + + val f = cache.logger.use { + logger.log(s"Getting $url") + cache.file(Artifact(url).withChanging(changing)).run.flatMap { + case Left(e) => Task.fail(e) + case Right(f) => Task.point(os.Path(f, os.pwd)) + }.unsafeRun()(cache.ec) + } + logger.debug(s"$url is available locally at $f") + + val launcher = + // FIXME Once coursier has proper support for extracted archives in cache, use it instead of those hacks + if (f.last.endsWith(".zip")) { + val baseDir = f / os.up + val dir = baseDir / s".${f.last.stripSuffix(".zip")}-content" + if (os.exists(dir)) + logger.debug(s"Found $dir") + else { + logger.debug(s"Unzipping $f under $dir") + val tmpDir = baseDir / s".${f.last.stripSuffix(".zip")}-content-${UUID.randomUUID()}" + try { + coursier.jvm.UnArchiver.default().extract( + ArchiveType.Zip, + f.toIO, + tmpDir.toIO, + overwrite = false + ) + if (!os.exists(dir)) { + try os.move(tmpDir, dir, atomicMove = true) + catch { + case ex: IOException => + if (!os.exists(dir)) + throw new Exception(ex) + } + } + } + finally { + try os.remove.all(tmpDir) + catch { + case _: IOException if Properties.isWin => + } + } + } + + val dirContent = os.list(dir) + if (dirContent.length == 1) dirContent.head + else dirContent.filter(_.last.startsWith(launcherPrefix)).head + } + else if (f.last.endsWith(".gz")) { + val dest = f / os.up / s".${f.last.stripSuffix(".gz")}" + if (os.exists(dest)) + logger.debug(s"Found $dest") + else { + logger.debug(s"Uncompression $f at $dest") + var fis: FileInputStream = null + var fos: FileOutputStream = null + var gzis: GZIPInputStream = null + try { + fis = new FileInputStream(f.toIO) + gzis = new GZIPInputStream(fis) + fos = new FileOutputStream(dest.toIO) + + val buf = Array.ofDim[Byte](16 * 1024) + var read = -1 + while ({ + read = gzis.read(buf) + read >= 0 + }) { + if (read > 0) + fos.write(buf, 0, read) + } + fos.flush() + } + finally { + if (gzis != null) gzis.close() + if (fos != null) fos.close() + if (fis != null) fis.close() + } + } + dest + } + else + f + + if (!Properties.isWin) + os.perms.set(launcher, "rwxr-xr-x") + + launcher + } + + def platformSuffix(supportsMusl: Boolean = true): String = { + val arch = sys.props("os.arch").toLowerCase(Locale.ROOT) match { + case "amd64" => "x86_64" + case other => other + } + val os = + if (Properties.isWin) "pc-win32" + else if (Properties.isLinux) { + if (supportsMusl && OsLibc.isMusl.getOrElse(false)) + "pc-linux-static" + else + "pc-linux" + } + else if (Properties.isMac) "apple-darwin" + else sys.error(s"Unrecognized OS: ${sys.props("os.name")}") + s"$arch-$os" + } + +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala index a2e93ce633..5e253f0047 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala @@ -15,6 +15,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.io.Codec import scala.util.control.NonFatal +import scala.util.Properties abstract class BspTestDefinitions(val scalaVersionOpt: Option[String]) extends munit.FunSuite with TestScalaVersionArgs { @@ -401,7 +402,10 @@ abstract class BspTestDefinitions(val scalaVersionOpt: Option[String]) ) val root = inputs.root() - withBsp(root, Seq(".")) { (localClient, remoteServer) => + val extraArgs = + if (Properties.isWin) Seq("-v", "-v", "-v") + else Nil + withBsp(root, Seq(".") ++ extraArgs) { (localClient, remoteServer) => async { val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala) val target = { diff --git a/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala b/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala new file mode 100644 index 0000000000..d58a55e516 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/FmtTests.scala @@ -0,0 +1,49 @@ +package scala.cli.integration + +import com.eed3si9n.expecty.Expecty.expect + +class FmtTests extends munit.FunSuite { + + val simpleInputs = TestInputs( + Seq( + os.rel / ".scalafmt.conf" -> + """runner.dialect = scala213 + |""".stripMargin, + os.rel / "Foo.scala" -> + """package foo + | + | object Foo extends java.lang.Object { + | def get() = 2 + | } + |""".stripMargin + ) + ) + val expectedSimpleInputsFormattedContent = noCrLf { + """package foo + | + |object Foo extends java.lang.Object { + | def get() = 2 + |} + |""".stripMargin + } + + private def noCrLf(input: String): String = + input.replaceAll("\r\n", "\n") + + test("simple") { + simpleInputs.fromRoot { root => + os.proc(TestUtil.cli, "fmt", ".").call(cwd = root) + val updatedContent = noCrLf(os.read(root / "Foo.scala")) + expect(updatedContent == expectedSimpleInputsFormattedContent) + } + } + + test("no inputs") { + simpleInputs.fromRoot { root => + os.proc(TestUtil.cli, "fmt").call(cwd = root) + val updatedContent = noCrLf(os.read(root / "Foo.scala")) + expect(updatedContent == expectedSimpleInputsFormattedContent) + } + } + +} diff --git a/project/deps.sc b/project/deps.sc index 14c8f7974c..d506325b59 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -52,6 +52,7 @@ object Deps { def scala3Compiler(sv: String) = ivy"org.scala-lang::scala3-compiler:$sv" def scalaAsync = ivy"org.scala-lang.modules::scala-async:0.10.0" def scalac(sv: String) = ivy"org.scala-lang:scala-compiler:$sv" + def scalafmtCli = ivy"org.scalameta::scalafmt-cli:3.0.3" def scalaJsEnvNodeJs = ivy"org.scala-js::scalajs-env-nodejs:1.1.1" def scalaJsLinker = ivy"org.scala-js::scalajs-linker:${Versions.scalaJs}" def scalaJsLinkerInterface = ivy"org.scala-js::scalajs-linker-interface:${Versions.scalaJs}" diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 267b300d6a..d705b0d0a2 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -53,6 +53,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -126,6 +127,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -156,6 +158,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -192,6 +195,28 @@ Available in commands: Aliases: `-o` +## Fmt options + +Available in commands: +- [`fmt`](./commands#fmt) + + + + +#### `--check` + +Check that sources are well formatted + +#### `--os-arch-suffix` + +#### `--scalafmt-tag` + +#### `--scalafmt-git-hub-org-name` + +#### `--scalafmt-extension` + +#### `--scalafmt-launcher` + ## Help options Available in commands: @@ -204,6 +229,7 @@ Available in commands: - [`compile`](./commands#compile) - [`directories`](./commands#directories) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`install completions`](./commands#install-completions) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) @@ -280,6 +306,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -321,6 +348,7 @@ Available in commands: - [`clean`](./commands#clean) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`install completions`](./commands#install-completions) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) @@ -588,6 +616,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -620,6 +649,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -658,6 +688,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -698,6 +729,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) - [`export`](./commands#export) +- [`fmt`](./commands#fmt) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index fe763e5cea..4dd840876e 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -62,6 +62,22 @@ Accepts options: - [scalac](./cli-options.md#scalac-options) - [shared](./cli-options.md#shared-options) +## `fmt` + +Format Scala code + +Accepts options: +- [compilation server](./cli-options.md#compilation-server-options) +- [coursier](./cli-options.md#coursier-options) +- [dependency](./cli-options.md#dependency-options) +- [fmt](./cli-options.md#fmt-options) +- [jvm](./cli-options.md#jvm-options) +- [logging](./cli-options.md#logging-options) +- [Scala.JS](./cli-options.md#scalajs-options) +- [Scala Native](./cli-options.md#scala-native-options) +- [scalac](./cli-options.md#scalac-options) +- [shared](./cli-options.md#shared-options) + ## `install completions` Accepts options: