Skip to content

Commit

Permalink
Merge pull request #1027 from alexarchambault/default-command
Browse files Browse the repository at this point in the history
Add "default" command
  • Loading branch information
alexarchambault authored May 25, 2022
2 parents c47fbf1 + 75a4027 commit 2a448d3
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 3 deletions.
23 changes: 22 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import $file.project.deps, deps.customRepositories
import $file.project.website

import java.io.File
import java.net.URL
import java.nio.charset.Charset
import java.util.Locale

Expand All @@ -29,7 +30,7 @@ import io.github.alexarchambault.millnativeimage.upload.Upload
import mill._, scalalib.{publish => _, _}
import mill.contrib.bloop.Bloop

import _root_.scala.util.Properties
import _root_.scala.util.{Properties, Using}

// Tell mill modules are under modules/
implicit def millModuleBasePath: define.BasePath =
Expand Down Expand Up @@ -644,6 +645,7 @@ trait Cli extends SbtModule with ProtoBuildModule with CliLaunchers
|/** Build-time constants. Generated by mill. */
|object Constants {
| def launcherTypeResourcePath = "${launcherTypeResourcePath.toString}"
| def defaultFilesResourcePath = "$defaultFilesResourcePath"
|}
|""".stripMargin
if (!os.isFile(dest) || os.read(dest) != code)
Expand All @@ -652,6 +654,25 @@ trait Cli extends SbtModule with ProtoBuildModule with CliLaunchers
}
def generatedSources = super.generatedSources() ++ Seq(constantsFile())

def defaultFilesResources = T.persistent {
val dir = T.dest / "resources"
val resources = Seq(
"https://raw.githubusercontent.com/scala-cli/default-workflow/main/.github/workflows/ci.yml" -> (os.sub / "workflows" / "default.yml"),
"https://raw.githubusercontent.com/scala-cli/default-workflow/main/.gitignore" -> (os.sub / "gitignore")
)
for ((srcUrl, destRelPath) <- resources) {
val dest = dir / defaultFilesResourcePath / destRelPath
if (!os.isFile(dest)) {
val content = Using.resource(new URL(srcUrl).openStream())(_.readAllBytes())
os.write(dest, content, createFolders = true)
}
}
PathRef(dir)
}
override def resources = T.sources {
super.resources() ++ Seq(defaultFilesResources())
}

def myScalaVersion: String

def scalaVersion = T(myScalaVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PersistentDiagnosticLogger(parent: Logger) extends Logger {

def diagnostics = diagBuilder.result()

def error(message: String): Unit = parent.error(message)
// TODO Use macros for log and debug calls to have zero cost when verbosity <= 0
def message(message: => String): Unit = parent.message(message)
def log(s: => String): Unit = parent.log(s)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class BuildProjectTests extends munit.FunSuite {

var diagnostics: List[Diagnostic] = Nil

override def error(message: String): Unit = ???

override def message(message: => String): Unit = ???

override def log(s: => String): Unit = ???
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ case class TestLogger(info: Boolean = true, debug: Boolean = false) extends Logg
}
}

def error(message: String): Unit =
System.err.println(message)
def message(message: => String): Unit =
if (info)
System.err.println(message)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package scala.cli.commands.default

import caseapp._

import scala.cli.commands.LoggingOptions

// format: off
final case class DefaultFileOptions(
@Recurse
logging: LoggingOptions = LoggingOptions(),
@Group("Default")
@HelpMessage("Write result to files rather than to stdout")
write: Boolean = false,
@Group("Default")
@HelpMessage("List available default files")
list: Boolean = false,
@Group("Default")
@HelpMessage("List available default file ids")
listIds: Boolean = false,
@Group("Default")
@HelpMessage("Force overwriting destination files")
@ExtraName("f")
force: Boolean = false
)
// format: on

object DefaultFileOptions {
implicit lazy val parser: Parser[DefaultFileOptions] = Parser.derive
implicit lazy val help: Help[DefaultFileOptions] = Help.derive
}
2 changes: 2 additions & 0 deletions modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.nio.file.InvalidPathException

import scala.cli.commands._
import scala.cli.commands.bloop.BloopOutput
import scala.cli.commands.default.DefaultFile
import scala.cli.commands.github.{SecretCreate, SecretList}
import scala.cli.commands.pgp.{PgpCommands, PgpCommandsSubst, PgpPull, PgpPush}
import scala.cli.commands.publish.{Publish, PublishLocal}
Expand All @@ -33,6 +34,7 @@ class ScalaCliCommands(
Bsp,
Clean,
Compile,
DefaultFile,
Directories,
Doc,
Doctor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package scala.cli.commands.default

import caseapp.core.RemainingArgs

import java.io.File

import scala.build.Logger
import scala.cli.commands.ScalaCommand
import scala.cli.commands.util.CommonOps._
import scala.cli.internal.Constants
import scala.util.Using

object DefaultFile extends ScalaCommand[DefaultFileOptions] {

override def hidden = true
override def inSipScala = false

private def readDefaultFile(path: String): Array[Byte] = {
val resourcePath = Constants.defaultFilesResourcePath + "/" + path
val cl = Thread.currentThread().getContextClassLoader
val resUrl = cl.getResource(resourcePath)
if (resUrl == null)
sys.error(s"Should not happen - resource $resourcePath not found")
Using.resource(resUrl.openStream())(_.readAllBytes())
}

final case class DefaultFile(
path: os.SubPath,
content: () => Array[Byte]
) {
def printablePath = path.segments.mkString(File.separator)
}

def defaultWorkflow: Array[Byte] =
readDefaultFile("workflows/default.yml")
def defaultGitignore: Array[Byte] =
readDefaultFile("gitignore")

val defaultFiles = Map(
"workflow" -> DefaultFile(os.sub / ".github" / "workflows" / "ci.yml", () => defaultWorkflow),
"gitignore" -> DefaultFile(os.sub / ".gitignore", () => defaultGitignore)
)
val defaultFilesByRelPath = defaultFiles.flatMap {
case (_, d) =>
// d.path.toString and d.printablePath differ on Windows (one uses '/', the other '\')
Seq(
d.path.toString -> d,
d.printablePath -> d
)
}

private def unrecognizedFile(name: String, logger: Logger): Nothing = {
logger.error(
s"Error: unrecognized default file $name (available: ${defaultFiles.keys.toVector.sorted.mkString(", ")})"
)
sys.exit(1)
}

def run(options: DefaultFileOptions, args: RemainingArgs): Unit = {

val logger = options.logging.logger

lazy val allArgs = {
val l = args.all
if (l.isEmpty) {
logger.error("No default file asked")
sys.exit(1)
}
l
}

if (options.list || options.listIds)
for ((name, d) <- defaultFiles.toVector.sortBy(_._1)) {
if (options.listIds)
println(name)
if (options.list)
println(d.printablePath)
}
else if (options.write)
for (arg <- allArgs)
defaultFiles.get(arg).orElse(defaultFilesByRelPath.get(arg)) match {
case Some(f) =>
val dest = os.pwd / f.path
if (!options.force && os.exists(dest)) {
logger.error(
s"Error: ${f.path} already exists. Pass --force to force erasing it."
)
sys.exit(1)
}
if (options.force)
os.write.over(dest, f.content(), createFolders = true)
else
os.write(dest, f.content(), createFolders = true)
logger.message(s"Wrote ${f.path}")
case None =>
unrecognizedFile(arg, logger)
}
else {
if (allArgs.length > 1) {
logger.error(s"Error: expected only one argument, got ${allArgs.length}")
sys.exit(1)
}

val arg = allArgs.head
val f = defaultFiles.get(arg).orElse(defaultFilesByRelPath.get(arg)).getOrElse {
unrecognizedFile(arg, logger)
}
System.out.write(f.content())
}
}
}
2 changes: 2 additions & 0 deletions modules/cli/src/main/scala/scala/cli/internal/CliLogger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class CliLogger(
}
}

def error(message: String) =
out.println(message)
def message(message: => String) =
if (verbosity >= 0)
out.println(message)
Expand Down
2 changes: 2 additions & 0 deletions modules/core/src/main/scala/scala/build/Logger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import scala.build.errors.{BuildException, Diagnostic, Severity}
import scala.scalanative.{build => sn}

trait Logger {
def error(message: String): Unit
// TODO Use macros for log and debug calls to have zero cost when verbosity <= 0
def message(message: => String): Unit
def log(s: => String): Unit
Expand Down Expand Up @@ -39,6 +40,7 @@ trait Logger {

object Logger {
private class Nop extends Logger {
def error(message: String): Unit = ()
def message(message: => String): Unit = ()
def log(s: => String): Unit = ()
def log(s: => String, debug: => String): Unit = ()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package scala.cli.integration

import com.eed3si9n.expecty.Expecty.expect

class DefaultFileTests extends munit.FunSuite {

test("Print .gitignore") {
val res = os.proc(TestUtil.cli, "default-file", ".gitignore")
.call()
val output = res.out.text()
expect(output.linesIterator.toVector.contains("/.scala-build/"))
}

test("Write .gitignore") {
TestInputs(Nil).fromRoot { root =>
os.proc(TestUtil.cli, "default-file", ".gitignore", "--write")
.call(cwd = root, stdout = os.Inherit)
val dest = root / ".gitignore"
expect(os.isFile(dest))
val content = os.read(dest)
expect(content.linesIterator.toVector.contains("/.scala-build/"))
}
}

}
6 changes: 4 additions & 2 deletions project/settings.sc
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def getGhToken(): String =
trait CliLaunchers extends SbtModule { self =>

def launcherTypeResourcePath = os.rel / "scala" / "cli" / "internal" / "launcher-type.txt"
def defaultFilesResourcePath = os.rel / "scala" / "cli" / "commands" / "publish"

trait CliNativeImage extends NativeImage {
def launcherKind: String
Expand All @@ -155,6 +156,7 @@ trait CliLaunchers extends SbtModule { self =>
Seq(
s"-H:IncludeResources=$localRepoResourcePath",
s"-H:IncludeResources=$launcherTypeResourcePath",
s"-H:IncludeResources=$defaultFilesResourcePath/.*",
"-H:-ParseRuntimeOptions",
s"-H:CLibraryPath=$cLibPath"
)
Expand Down Expand Up @@ -730,10 +732,10 @@ private def doFormatNativeImageConf(dir: os.Path, format: Boolean): List[os.Path
trait FormatNativeImageConf extends JavaModule {
def nativeImageConfDirs = T {
resources()
.map(_.path)
.map(_.path / "META-INF" / "native-image")
.filter(os.exists(_))
.flatMap { path =>
os.walk(path / "META-INF" / "native-image")
os.walk(path)
.filter(_.last.endsWith("-config.json"))
.filter(os.isFile(_))
.map(_ / os.up)
Expand Down
4 changes: 4 additions & 0 deletions website/docs/commands/misc/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Miscellaneous",
"position": 17
}
28 changes: 28 additions & 0 deletions website/docs/commands/misc/default-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Default File
sidebar_position: 1
---

The `default-file` sub-command provides sensible default content for files
such as `.gitignore` or for GitHub actions workflows, for Scala CLI projects.

To list the available files, pass it `--list`:
```text
$ scala-cli default-file --list
.gitignore
.github/workflows/ci.yml
```

Get the content of a default file with
```text
$ scala-cli default-file .gitignore
/.bsp/
/.scala-build/
```

Optionally, write the content of one or more default files by passing `--write`:
```text
$ scala-cli default-file --write .gitignore .github/workflows/ci.yml
Wrote .gitignore
Wrote .github/workflows/ci.yml
```
Loading

0 comments on commit 2a448d3

Please sign in to comment.