Skip to content

Commit

Permalink
Add "default" command
Browse files Browse the repository at this point in the history
Giving the content of default .gitignore or .github/workflows/ci.yml
files. Use like
```
$ scala-cli default gitignore
/.bsp/
/.scala-build/

$ scala-cli default workflow --write
$ cat .github/workflows/ci.yml
…
```
  • Loading branch information
alexarchambault committed May 24, 2022
1 parent e844754 commit 75a4027
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 1 deletion.
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
@@ -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())
}
}
}
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/"))
}
}

}
2 changes: 2 additions & 0 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
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
```
29 changes: 29 additions & 0 deletions website/docs/reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,32 @@ Available in commands:

Aliases: `-X`

## Default file options

Available in commands:
- [`default-file`](./commands.md#default-file)


<!-- Automatically generated, DO NOT EDIT MANUALLY -->

#### `--write`

Write result to files rather than to stdout

#### `--list`

List available default files

#### `--list-ids`

List available default file ids

#### `--force`

Aliases: `-f`

Force overwriting destination files

## Dependency options

Available in commands:
Expand Down Expand Up @@ -433,6 +459,7 @@ Available in commands:
- [`bsp`](./commands.md#bsp)
- [`clean`](./commands.md#clean)
- [`compile`](./commands.md#compile)
- [`default-file`](./commands.md#default-file)
- [`directories`](./commands.md#directories)
- [`doc`](./commands.md#doc)
- [`doctor`](./commands.md#doctor)
Expand Down Expand Up @@ -661,6 +688,7 @@ Available in commands:
- [`bsp`](./commands.md#bsp)
- [`clean`](./commands.md#clean)
- [`compile`](./commands.md#compile)
- [`default-file`](./commands.md#default-file)
- [`doc`](./commands.md#doc)
- [`export`](./commands.md#export)
- [`fmt` / `format` / `scalafmt`](./commands.md#fmt)
Expand Down Expand Up @@ -1568,6 +1596,7 @@ Available in commands:
- [`bsp`](./commands.md#bsp)
- [`clean`](./commands.md#clean)
- [`compile`](./commands.md#compile)
- [`default-file`](./commands.md#default-file)
- [`directories`](./commands.md#directories)
- [`doc`](./commands.md#doc)
- [`doctor`](./commands.md#doctor)
Expand Down
7 changes: 7 additions & 0 deletions website/docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,13 @@ Accepts options:
- [verbosity](./cli-options.md#verbosity-options)
- [workspace](./cli-options.md#workspace-options)
### `default-file`
Accepts options:
- [default file](./cli-options.md#default-file-options)
- [logging](./cli-options.md#logging-options)
- [verbosity](./cli-options.md#verbosity-options)
### `directories`
Prints directories used by `scala-cli`
Expand Down

0 comments on commit 75a4027

Please sign in to comment.