Skip to content
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ mops wrapper
```

It prints the path of each `mopsw` (or `mopsw.cmd` on Windows) it writes. Run the wrapper in place of `mops`. Without a
wrapper, pass `--mps-home PATH` and, when the distribution bundles no Java, `--java-home PATH`.
Gradle build, pass the paths yourself and mops writes the wrapper into the MPS project's `build` directory:

```sh
mops --mps-home PATH --java-home PATH wrapper
```

Here and without a wrapper, `--java-home` is needed only when the MPS distribution bundles no Java.

## A short tour

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlin.test.*

/**
* Runs the installed `mops wrapper` on a Gradle build whose MPS project loads its project library through a path macro,
* then runs a command through the written `mopsw`.
* and on an MPS project without a Gradle build, then runs a command through the written `mopsw`.
*/
@ResourceLock("system-streams")
class WrapperIntegrationTest {
Expand Down Expand Up @@ -55,6 +55,32 @@ class WrapperIntegrationTest {
} finally { stopDaemons(project, home) }
}

@Test
fun `given MPS and Java homes write a working wrapper without a Gradle build`() {
val project = copyTestProject("project-library", tempDir.resolve("plain/project")).toRealPath()
val home = tempDir.resolve("daemon-home").createDirectories()
val script = tempDir.resolve("library.groovy")
script.writeText("project.read { mops.lookup.requireModule('mops.library') }\nreturn 'ok'\n")
try {
val written = runProcess(
project,
installedMops(
"--mps-home", System.getProperty("test.mpsHome"),
"--java-home", System.getProperty("test.jbrHome"),
"wrapper",
),
)
assertEquals(0, written.exitCode, written.output)
assertContains(written.stdout, "Wrapper: build/mopsw")
val result = runProcess(
project,
listOf("sh", "build/mopsw", "--daemon-home", home.pathString, "code", "run", script.pathString),
)
assertEquals(0, result.exitCode, result.output)
assertEquals("ok", result.stdout.trim(), result.output)
} finally { stopDaemons(project, home) }
}

/** Writes a Gradle build at [root] whose runtime discovery finds the MPS and Java homes of the test run. */
private fun gradleBuild(root: Path): Path {
val mpsHome = Path.of(System.getProperty("test.mpsHome")).invariantSeparatorsPathString
Expand Down
19 changes: 11 additions & 8 deletions cli/src/main/kotlin/com/specificlanguages/mops/cli/MopsCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,7 @@ class MopsCommand(
fun createDaemonContext(projectPathHint: Path): DaemonContext {
val resolvedMpsHome = resolveMpsHome()?.also { requireHome("MPS home", it) }
?: throw MpsHomeRequiredException()
val resolvedJavaHome =
javaHome?.let(workingDirectory::resolve)?.also { requireHome("Java home", it) }
?: MpsDistributionLayout.findBundledJavaHome(resolvedMpsHome)
?: throw IllegalStateException(
"MPS distribution at $resolvedMpsHome does not bundle Java; pass --java-home <path>, " +
"or run mops wrapper in a supported Gradle project and then run the wrapper path it prints"
)
val resolvedJavaHome = resolveJavaHome(resolvedMpsHome).also { requireHome("Java home", it) }

val projectPath = resolveProjectPath(projectPathHint)

Expand All @@ -168,7 +162,16 @@ class MopsCommand(
)
}

fun resolveMpsHome(): Path? = mpsHome?.let(workingDirectory::resolve)
fun resolveMpsHome(): Path? = mpsHome?.let { workingDirectory.resolve(it).normalize() }

/** `--java-home`, or else the Java home bundled with [mpsHome]. */
fun resolveJavaHome(mpsHome: Path): Path =
javaHome?.let { workingDirectory.resolve(it).normalize() }
?: MpsDistributionLayout.findBundledJavaHome(mpsHome)
?: throw IllegalStateException(
"MPS distribution at $mpsHome does not bundle Java; pass --java-home <path>, " +
"or run mops wrapper in a supported Gradle project and then run the wrapper path it prints"
)

private fun requireHome(homeName: String, home: Path) {
if (!home.exists()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ internal class GradleHomeDiscovery {

val root = discoverer.findDirectoryContaining("settings.gradle", "settings.gradle.kts")
?: discoverer.findDirectoryContaining("build.gradle", "build.gradle.kts")
?: error("No Gradle build found at or above $directory.")
?: error(
"No Gradle build found at or above $directory. " +
"Pass --mps-home, and --java-home unless MPS bundles Java, to write the wrapper without Gradle."
)
val isWindows = System.getProperty("os.name").lowercase().contains("win")

val wrapperName = if (isWindows) "gradlew.bat" else "gradlew"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,26 @@ import java.util.concurrent.Callable

@Command(
name = "wrapper",
header = ["Write a project-local mops wrapper using Gradle-discovered paths."],
header = ["Write a project-local mops wrapper from Gradle or given paths."],
mixinStandardHelpOptions = true,
description = [
"Write a project-local mops wrapper using runtime paths discovered from Gradle.",
"Write a project-local mops wrapper using runtime paths discovered from Gradle, or given with --mps-home " +
"and --java-home.",
"",
"Uses the nearest Gradle wrapper to inspect all projects in its build and writes mopsw or mopsw.cmd for " +
"every discovered MPS project. Pass --project-root to write only that project's wrapper. Wrappers go " +
"directly in each Gradle project's build directory when it has one detected MPS project; with multiple " +
"MPS projects, they go in mops/<MPS-project-name> beneath it. Reported paths are relative to the " +
"working directory.",
"",
"No CLI-configured MPS home or daemon is required; Java must be available to run Gradle. Querying " +
"configured providers may download and extract MPS or JBR distributions; it does not run preparation " +
"When the global option --mps-home is given, mops wrapper uses it and --java-home and does not run " +
"Gradle, so the project needs no Gradle build. Without --java-home, it uses the Java runtime bundled " +
"with MPS. It writes the wrapper of one MPS project: --project-root, or else the nearest directory at " +
"or above PATH that contains .mps. The wrapper goes in the build directory of the MPS project, such as " +
"build/mopsw.",
"",
"Gradle discovery needs no daemon; Java must be available to run Gradle. Querying configured providers " +
"may download and extract MPS or JBR distributions; it does not run preparation " +
"or language build task actions. If discovery finds only some paths, it prints them, writes no " +
"wrapper, and exits with status 1. When both paths are known, it writes the wrappers and exits with " +
"status 0, with warnings when the paths are not usable yet, so wrappers can be written before the " +
Expand Down Expand Up @@ -68,7 +75,10 @@ class WrapperCommand(
) : Callable<Int> {
@Parameters(
index = "0", arity = "0..1", paramLabel = "PATH",
description = ["Starting point of discovery; default is --project-root when supplied, otherwise the working directory."]
description = [
"Starting point of discovery, or of the MPS project search with --mps-home; default " +
"is --project-root when supplied, otherwise the working directory.",
]
)
var path: String? = null

Expand All @@ -85,32 +95,10 @@ class WrapperCommand(
override fun call(): Int {
spec.commandLine().out.println("mops version: ${MopsBuild.fullVersion}")
val start = root.workingDirectory.resolve(path ?: root.projectRoot ?: ".").toRealPath()
val discovery = GradleHomeDiscovery()
val guesses = discovery.discover(start, spec.commandLine().err)
val selectedProjectRoot = root.projectRoot?.let { root.resolveProjectPath(start) }
val targets = if (selectedProjectRoot == null) {
guesses.flatMap { guess -> guess.mpsProjectRoots.map { guess to it } }
.distinctBy { it.second }
} else {
val owner = guesses.filter { selectedProjectRoot.startsWith(it.projectDir) }
.maxByOrNull { it.projectDir.nameCount }
?: guesses.firstOrNull { selectedProjectRoot in it.mpsProjectRoots }
?: error("MPS project is not part of the discovered Gradle build: ${displayPath(selectedProjectRoot, root.workingDirectory)}")
listOf(owner to selectedProjectRoot)
}
val targetRuntime = targets.singleOrNull()?.first?.takeIf { it.source != null }
val preferredRuntime = targetRuntime?.takeIf { it.hasKnownHomes }
val runtime = preferredRuntime
?: guesses.firstOrNull { it.source != null && it.hasKnownHomes }
?: targetRuntime
?: guesses.firstOrNull { it.source != null }
?: error(
"No supported runtime configuration found in this Gradle build. " +
"Pass --mps-home and --java-home directly to other mops commands."
)
val mpsHome = root.resolveMpsHome()
val (runtime, targets) = if (mpsHome != null) givenRuntime(start, mpsHome) else discoveredRuntime(start)
val out = spec.commandLine().out
val err = spec.commandLine().err
out.println("Source: ${runtime.source} in ${displayPath(runtime.projectDir, root.workingDirectory)}")
out.println("MPS home: ${runtime.mpsHome?.let { displayPath(it, root.workingDirectory) } ?: "unknown"}")
out.println("Java home: ${runtime.javaHome?.let { displayPath(it, root.workingDirectory) } ?: "unknown"}")
if (runtime.mpsHome != null && !runtime.usableMps) {
Expand All @@ -131,6 +119,51 @@ class WrapperCommand(
return if (runtime.hasKnownHomes && targets.isNotEmpty()) 0 else 1
}

/**
* The given homes and the MPS project found the way other commands find it. The project directory stands in for
* the Gradle project, so the wrapper goes in its build directory.
*/
private fun givenRuntime(start: Path, mpsHome: Path): WrapperPlan {
val projectRoot = root.resolveProjectPath(start)
val runtime = HomeGuess(
projectDir = projectRoot,
buildDir = projectRoot.resolve("build"),
source = null,
mpsHome = mpsHome,
javaHome = root.resolveJavaHome(mpsHome),
mpsProjectRoots = listOf(projectRoot),
)
spec.commandLine().out.println("Source: --mps-home${if (root.javaHome != null) " and --java-home" else ""}")
return WrapperPlan(runtime, listOf(runtime to projectRoot))
}

private fun discoveredRuntime(start: Path): WrapperPlan {
val guesses = GradleHomeDiscovery().discover(start, spec.commandLine().err)
val selectedProjectRoot = root.projectRoot?.let { root.resolveProjectPath(start) }
val targets = if (selectedProjectRoot == null) {
guesses.flatMap { guess -> guess.mpsProjectRoots.map { guess to it } }
.distinctBy { it.second }
} else {
val owner = guesses.filter { selectedProjectRoot.startsWith(it.projectDir) }
.maxByOrNull { it.projectDir.nameCount }
?: guesses.firstOrNull { selectedProjectRoot in it.mpsProjectRoots }
?: error("MPS project is not part of the discovered Gradle build: ${displayPath(selectedProjectRoot, root.workingDirectory)}")
listOf(owner to selectedProjectRoot)
}
val targetRuntime = targets.singleOrNull()?.first?.takeIf { it.source != null }
val preferredRuntime = targetRuntime?.takeIf { it.hasKnownHomes }
val runtime = preferredRuntime
?: guesses.firstOrNull { it.source != null && it.hasKnownHomes }
?: targetRuntime
?: guesses.firstOrNull { it.source != null }
?: error(
"No supported runtime configuration found in this Gradle build. " +
"Pass --mps-home, and --java-home unless MPS bundles Java, to write the wrapper without Gradle."
)
spec.commandLine().out.println("Source: ${runtime.source} in ${displayPath(runtime.projectDir, root.workingDirectory)}")
return WrapperPlan(runtime, targets)
}

/** Writes a wrapper for each target project; all path macros are settled before the first wrapper is written. */
private fun writeWrappers(runtime: HomeGuess, targets: List<Pair<HomeGuess, Path>>) {
val out = spec.commandLine().out
Expand Down Expand Up @@ -163,4 +196,7 @@ class WrapperCommand(
}
return directory.resolve(wrapperName)
}

/** The runtime whose homes the wrappers pass, and each MPS project to write a wrapper for with its owner. */
private data class WrapperPlan(val runtime: HomeGuess, val targets: List<Pair<HomeGuess, Path>>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import org.junit.jupiter.api.io.TempDir
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.file.Path
import kotlin.io.path.*
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue

class WrapperCommandTest {
@TempDir
Expand Down Expand Up @@ -38,4 +41,88 @@ class WrapperCommandTest {

assertEquals("mops version: ${MopsBuild.fullVersion}", out.toString().lineSequence().first())
}

@Test
fun `given MPS and Java homes write the wrapper of the MPS project without Gradle`() {
val temporary = temporary.toRealPath()
val project = temporary.resolve("MPS project").also { it.resolve(".mps").createDirectories() }
val mpsHome = temporary.resolve("mps").createDirectories()
val javaHome = temporary.resolve("jbr").also { home ->
home.resolve("bin").createDirectories()
home.resolve("bin/java").createFile().toFile().setExecutable(true)
}
val out = StringWriter()
val err = StringWriter()
val command = newCommandLine(workingDirectory = project.resolve(".mps")) { Path.of("/mops bin/mops") }
command.out = PrintWriter(out)
command.err = PrintWriter(err)

val exitCode = command.execute("--mps-home", mpsHome.toString(), "--java-home", "../../jbr", "wrapper")

assertEquals(0, exitCode, "$out$err")
assertEquals("", err.toString())
val wrapper = project.resolve("build/mopsw")
assertEquals(
"#!/bin/sh\nexec '/mops bin/mops' --mps-home='$mpsHome' --java-home='$javaHome' --project-root='$project' \"\$@\"\n",
wrapper.readText(),
)
assertTrue(wrapper.isExecutable())
assertEquals(
listOf(
"mops version: ${MopsBuild.fullVersion}",
"Source: --mps-home and --java-home",
"MPS home: ../../mps",
"Java home: ../../jbr",
"Project root: ..",
"Wrapper: ../build/mopsw",
),
out.toString().lines().filter { it.isNotEmpty() },
)
}

@Test
fun `given homes that are not usable yet still write the wrapper with warnings`() {
val temporary = temporary.toRealPath()
val project = temporary.resolve("project").also { it.resolve(".mps").createDirectories() }
val err = StringWriter()
val command = newCommandLine(workingDirectory = temporary) { Path.of("/mops") }
command.out = PrintWriter(StringWriter())
command.err = PrintWriter(err)
val output = temporary.resolve("out/run-mops")

val exitCode = command.execute(
"--mps-home", "missing-mps", "--java-home", "missing-jbr", "--project-root", "project",
"wrapper", "--output", "out/run-mops",
)

assertEquals(0, exitCode, err.toString())
assertEquals(
listOf(
"Warning: MPS home is missing: missing-mps",
"Warning: Java home is missing or has no usable bin/java: missing-jbr",
),
err.toString().lines().filter { it.isNotEmpty() },
)
assertEquals(
"#!/bin/sh\nexec '/mops' --mps-home='$temporary/missing-mps' --java-home='$temporary/missing-jbr' " +
"--project-root='$project' \"\$@\"\n",
output.readText(),
)
}

@Test
fun `an MPS home alone uses the Java home bundled with it, as other commands do`() {
val temporary = temporary.toRealPath()
val project = temporary.resolve("project").also { it.resolve(".mps").createDirectories() }
val mpsHome = temporary.resolve("mps")
mpsHome.resolve("jbr/Contents/Home/bin").createDirectories()
val err = StringWriter()
val command = newCommandLine(workingDirectory = project) { Path.of("/mops") }
command.out = PrintWriter(StringWriter())
command.err = PrintWriter(err)

assertEquals(0, command.execute("--mps-home", mpsHome.toString(), "wrapper"), err.toString())

assertContains(project.resolve("build/mopsw").readText(), "--java-home='$mpsHome/jbr")
}
}
4 changes: 2 additions & 2 deletions docs/reference/index.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading