Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "default" command #1027

Merged
merged 3 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
alexarchambault marked this conversation as resolved.
Show resolved Hide resolved
"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