From bde4c1c943509ebb1e8b5d1bf23bef3809cafbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 20:02:33 +0100 Subject: [PATCH 1/2] fix(make): leave out modules without a generation target facet and always explain a failed make Make no longer hands MPS the models of a module without a generation target facet, such as a solution created with the not-generated preset; --messages info names the modules it leaves out. A make whose make sequence has no step (its models use no language) reports nothing to generate. A failed make that MPS reports no error for gets an error with the reason from the make result. Fixes #226 --- .../mops/cli/MakeIntegrationTest.kt | 16 +++ .../mops/cli/make/makeHelp.kt | 3 +- .../mops/daemon/ProjectMake.kt | 116 +++++++++++++----- .../mops/daemon/MakeResponseTest.kt | 47 +++++++ .../mops/daemon/MakeSemanticsTest.kt | 75 ++++++++++- docs/reference/make.md | 8 +- examples/pages/editing.creation.md | 8 +- .../mops/protocol/MakeOutcome.kt | 10 +- .../mops/protocol/daemonResponses.kt | 4 +- skills/mops/references/editing.creation.md | 8 +- 10 files changed, 246 insertions(+), 49 deletions(-) create mode 100644 daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeResponseTest.kt diff --git a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt index c7e76eb0..4893bcb6 100644 --- a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt +++ b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt @@ -7,6 +7,7 @@ import com.specificlanguages.mops.protocol.ProtocolJson import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.parallel.ResourceLock import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -29,6 +30,21 @@ class MakeIntegrationTest { ) } + @Test + fun `make module leaves out a solution created with the not-generated preset`() { + val project = daemons.project("mps-json", copy = "not-generated") + val created = project.run("create", "solution", "example.analyzed") + assertEquals(0, created.exitCode, created.output) + val model = project.run("create", "model", "example.analyzed.main", "--module", "example.analyzed") + assertEquals(0, model.exitCode, model.output) + + val result = project.run("make", "module", "--messages", "info", "example.analyzed") + + assertEquals(0, result.exitCode, result.output) + assertEquals("nothing to generate", result.stdout.trim(), result.output) + assertContains(result.stderr, "info\tnot generating the models of example.analyzed") + } + companion object { @JvmField @RegisterExtension diff --git a/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt b/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt index 5a772cb6..b343dd71 100644 --- a/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt +++ b/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt @@ -9,7 +9,8 @@ internal const val MAKE_OUTPUT_HELP = /** What a make deletes, for the help footers of both make commands. */ internal const val MAKE_CLEAN_HELP = "The make generates every model of the made modules and deletes files in their output folders that it no " + - "longer generates." + "longer generates. It leaves out the models of modules without a generation target facet, such as " + + "solutions created with the `not-generated` usage preset; `--messages info` names these modules." /** How the generation options behave, for the help footers of both make commands. */ internal const val MAKE_GENERATION_HELP = diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt index cf8a1883..2cee5c9b 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt @@ -5,22 +5,27 @@ import com.specificlanguages.mops.daemon.core.MpsRequestException import com.specificlanguages.mops.protocol.MakeGenerationOptions import com.specificlanguages.mops.protocol.MakeMessageJson import com.specificlanguages.mops.protocol.MakeMessageKind +import com.specificlanguages.mops.protocol.MakeMessageLevel import com.specificlanguages.mops.protocol.MakeOutcome import com.specificlanguages.mops.protocol.MakeResponse +import jetbrains.mps.generator.GenerationFacade import jetbrains.mps.generator.GenerationSettingsProvider import jetbrains.mps.generator.IModifiableGenerationSettings import jetbrains.mps.make.MakeSession import jetbrains.mps.make.dependencies.MakeSequence import jetbrains.mps.make.resources.IResource +import jetbrains.mps.make.script.IResult import jetbrains.mps.make.script.IScriptController import jetbrains.mps.make.service.CoreMakeTask import jetbrains.mps.messages.IMessage import jetbrains.mps.messages.IMessageHandler +import jetbrains.mps.messages.Message import jetbrains.mps.messages.MessageKind import jetbrains.mps.progress.EmptyProgressMonitor import jetbrains.mps.project.AbstractModule import jetbrains.mps.project.Project import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager +import jetbrains.mps.project.facets.GenerationTargetFacet import jetbrains.mps.smodel.Language import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory import jetbrains.mps.smodel.resources.ModelsToResources @@ -36,8 +41,9 @@ import org.jetbrains.mps.openapi.util.ProgressMonitor * The make framework builds exactly the modules whose models it is handed — it never pulls in a dependency that is not * in the set, it only orders what it is given. So to make un-made dependencies too, [makeModules] first expands the * requested modules to their transitive compile-dependency closure ([GlobalModuleDependenciesManager]); already-built - * library dependencies drop out because [ModelsToResources] keeps only generatable models. Every model handed to the - * make is generated; nothing skips unmodified models. + * library dependencies drop out because [ModelsToResources] keeps only generatable models. Models of modules without a + * generation target facet are left out too ([makeSet]). Every model handed to the make is generated; nothing skips + * unmodified models. * * Model collection runs inside short read actions; the make call runs outside any model action, on the calling thread, * because the make framework acquires its own model locks. @@ -95,43 +101,60 @@ class ProjectMake(private val project: Project) { monitor: ProgressMonitor, ): MakeResponse { val options = makeGenerationOptions(generationSettings, generation) - // resources() reads the model graph and keeps only generatable models, grouped one MResource per module. - // But "generatable" is not the same as "writable": MPS ships its library languages and runtime solutions as - // *source* models inside `-src.jar` files, and those models report themselves generatable, so they survive - // ModelsToResources — yet writing their generation output into the jar fails ("Write for jar files is not - // supported"). We only ever intend to make project sources, so drop models of read-only (packaged) modules - // first. See https://github.com/specificlanguages/mps-api-research/blob/main/make-generation.md. - val resources: List = project.modelAccess.computeReadAction> { - ModelsToResources(models.filter { it.isInWritableModule() }, /* cleanMake = */ true).resources().toList() - } - - if (resources.isEmpty()) { - return MakeResponse(MakeOutcome.NOTHING_TO_GENERATE, moduleCount = 0, messages = emptyList()) - } - + val makeSet = project.modelAccess.computeReadAction { makeSet(models) } val handler = CollectingMessageHandler() + makeSet.leftOutModules.forEach { handler.handle(Message(MessageKind.INFORMATION, it.notGeneratedMessage())) } + // MakeSession's own clean flag only names the IDE's make script; the resources above carry the clean make. val session = MakeSession(project, handler, /* cleanMake = */ false) + // The make sequence leaves out a module whose models use no language and import no language or devkit, so it + // can have no step even when there are resources. + val sequence = MakeSequence(makeSet.resources, null, session) + if (sequence.steps() == 0) { + return MakeResponse( + MakeOutcome.NOTHING_TO_GENERATE, + moduleCount = 0, + messages = handler.messages.filter { generation.messages.reports(it.kind) }, + ) + } + // Reimplements BuildMakeService.doMake using classes from mps-core: BuildMakeService itself ships only in // lib/mpsant/mps-tool.jar, which is not on the daemon classpath. A null script lets MPS build the correct // per-cluster script; a null-controller default is IScriptController.Stub2. See the mps-api-research notes. val controller: IScriptController = makeScriptController(session, options) - val task = CoreMakeTask(MakeSequence(resources, null, session), controller, handler) + val task = CoreMakeTask(sequence, controller, handler) val result = withMakeGenerationSettings(generationSettings, generation) { task.run(monitor) task.getResult() } - // MPS's make result is not reliable on its own — it can report success while errors were reported — so a run - // counts as successful only when the result is successful AND no error message was seen. - val succeeded = result != null && result.isSucessful && !handler.errorOccurred - return MakeResponse( - outcome = if (succeeded) MakeOutcome.SUCCESS else MakeOutcome.FAILED, - moduleCount = resources.size, - messages = handler.messages.filter { generation.messages.reports(it.kind) }, - ) + return makeResponse(result, handler.messages, makeSet.resources.size, generation.messages) + } + + /** + * The make resources for [models], grouped one per module, and the modules whose models are left out because they + * have nowhere to write generated files. Must run inside a read action. + * + * Models of read-only modules are left out: MPS ships its library languages and runtime solutions as *source* + * models inside `-src.jar` files, and those models report themselves generatable, yet writing their generation + * output into the jar fails ("Write for jar files is not supported"). Models whose module has no generation target + * facet are left out too: text generation has no output location for them, which fails the make on MPS before + * 2025.3. See https://github.com/specificlanguages/mps-api-research/blob/main/make-generation.md. + */ + private fun makeSet(models: List): MakeSet { + val writable = models.filter { it.isInWritableModule() } + val (withTarget, withoutTarget) = writable.partition { GenerationTargetFacet.find(it) != null } + val leftOutModules = withoutTarget.filter(GenerationFacade::canGenerate).mapNotNull { it.module }.distinct() + // resources() keeps only generatable models. + val resources = ModelsToResources(withTarget, /* cleanMake = */ true).resources().toList() + return MakeSet(resources, leftOutModules) } + private class MakeSet(val resources: List, val leftOutModules: List) + + private fun SModule.notGeneratedMessage(): String = + "not generating the models of $moduleName: the module has no generation target facet" + /** * The modules to make for a request over [requested]: the requested modules, their module-level compile-dependency * closure, and — because MPS treats a used language as an already-built dependency and never adds its source module @@ -168,8 +191,7 @@ class ProjectMake(private val project: Project) { } // A model belongs to a writable module when its owning module is not read-only. Read-only modules are the deployed, - // jar-packaged libraries MPS ships pre-built; they must never be regenerated. Models with no resolvable module are - // kept — they are not the packaged-library case this guards against. + // jar-packaged libraries MPS ships pre-built; they must never be regenerated. private fun SModel.isInWritableModule(): Boolean = (module as? AbstractModule)?.isReadOnly != true @@ -188,13 +210,11 @@ class ProjectMake(private val project: Project) { } /** - * Collects the messages MPS reports during the make and records whether any error was seen. A message with no kind - * is treated as an error, matching how MPS's own headless tooling handles it. + * Collects the messages MPS reports during the make. A message with no kind is treated as an error, matching how + * MPS's own headless tooling handles it. */ private class CollectingMessageHandler : IMessageHandler { val messages = mutableListOf() - var errorOccurred = false - private set override fun handle(msg: IMessage) { val kind = when (msg.kind) { @@ -202,9 +222,41 @@ class ProjectMake(private val project: Project) { MessageKind.WARNING -> MakeMessageKind.WARNING else -> MakeMessageKind.ERROR } - if (kind == MakeMessageKind.ERROR) errorOccurred = true messages.add(MakeMessageJson(kind, msg.text.orEmpty())) } } } +/** + * The response for a make that ran with [result] and reported [messages], keeping the messages that [level] reports. + * + * MPS's make result is not reliable on its own — it can report success while errors were reported — so a make + * succeeds only when [result] is successful and no error message was seen. A failed make always carries an error: when + * MPS reported none, the response says so with the reason from [result], if any. + */ +internal fun makeResponse( + result: IResult?, + messages: List, + moduleCount: Int, + level: MakeMessageLevel?, +): MakeResponse { + val hasError = messages.any { it.kind == MakeMessageKind.ERROR } + val succeeded = result != null && result.isSucessful && !hasError + val reported = messages.filter { level.reports(it.kind) } + return MakeResponse( + outcome = if (succeeded) MakeOutcome.SUCCESS else MakeOutcome.FAILED, + moduleCount = moduleCount, + messages = if (succeeded || hasError) reported else reported + unexplainedFailure(result), + ) +} + +private fun unexplainedFailure(result: IResult?): MakeMessageJson { + val reason = result?.message().orEmpty().ifBlank { null } + val text = if (reason == null) { + "make failed without reporting an error" + } else { + "make failed without reporting an error: $reason" + } + return MakeMessageJson(MakeMessageKind.ERROR, text) +} + diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeResponseTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeResponseTest.kt new file mode 100644 index 00000000..9553cf18 --- /dev/null +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeResponseTest.kt @@ -0,0 +1,47 @@ +package com.specificlanguages.mops.daemon + +import com.specificlanguages.mops.protocol.MakeMessageJson +import com.specificlanguages.mops.protocol.MakeMessageKind +import com.specificlanguages.mops.protocol.MakeMessageLevel +import com.specificlanguages.mops.protocol.MakeOutcome +import com.specificlanguages.mops.protocol.MakeResponse +import jetbrains.mps.make.script.IResult +import kotlin.test.Test +import kotlin.test.assertEquals + +/** The outcome and messages of a make, from the MPS make result and the messages the make reported. */ +class MakeResponseTest { + private val warning = MakeMessageJson(MakeMessageKind.WARNING, "a warning") + private val error = MakeMessageJson(MakeMessageKind.ERROR, "an error") + private val info = MakeMessageJson(MakeMessageKind.INFO, "an info") + + @Test + fun `a successful result without errors succeeds with the reported messages`() { + val response = makeResponse(IResult.SUCCESS(null), listOf(info, warning), moduleCount = 2, level = null) + + assertEquals(MakeResponse(MakeOutcome.SUCCESS, 2, listOf(warning)), response) + } + + @Test + fun `a successful result with an error fails with that error`() { + val response = makeResponse(IResult.SUCCESS(null), listOf(warning, error), moduleCount = 1, level = null) + + assertEquals(MakeResponse(MakeOutcome.FAILED, 1, listOf(warning, error)), response) + } + + @Test + fun `a failed result without an error message fails with the reason from the result`() { + val response = makeResponse(IResult.FAILURE("not started", null), listOf(warning), moduleCount = 1, level = null) + + val reason = MakeMessageJson(MakeMessageKind.ERROR, "make failed without reporting an error: not started") + assertEquals(MakeResponse(MakeOutcome.FAILED, 1, listOf(warning, reason)), response) + } + + @Test + fun `a missing result fails with an error even at the error message level`() { + val response = makeResponse(null, listOf(warning), moduleCount = 1, level = MakeMessageLevel.ERROR) + + val reason = MakeMessageJson(MakeMessageKind.ERROR, "make failed without reporting an error") + assertEquals(MakeResponse(MakeOutcome.FAILED, 1, listOf(reason)), response) + } +} diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt index d6cad295..2086c2a8 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt @@ -2,22 +2,36 @@ package com.specificlanguages.mops.daemon import com.specificlanguages.mops.daemon.core.MpsErrorCode import com.specificlanguages.mops.daemon.core.MpsRequestException +import com.specificlanguages.mops.protocol.CreateModelRequest +import com.specificlanguages.mops.protocol.CreateSolutionRequest +import com.specificlanguages.mops.protocol.EditBatch +import com.specificlanguages.mops.protocol.EditOperation import com.specificlanguages.mops.protocol.MakeGenerationOptions +import com.specificlanguages.mops.protocol.MakeMessageLevel +import com.specificlanguages.mops.protocol.MakeModulesRequest import com.specificlanguages.mops.protocol.MakeMessageKind import com.specificlanguages.mops.protocol.MakeOutcome +import com.specificlanguages.mops.protocol.MakeResponse +import com.specificlanguages.mops.protocol.ModelEditRequest +import com.specificlanguages.mops.protocol.ModelDestination +import com.specificlanguages.mops.protocol.MpsNodePropertyJson +import com.specificlanguages.mops.protocol.SolutionUsagePreset import java.nio.file.Path +import kotlin.io.path.exists import kotlin.io.path.readText import kotlin.io.path.writeText import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertIs import kotlin.test.assertTrue /** * Drives the real make against fixture-project copies (make writes generated files, so the shared read-only project * cannot be used). Asserts that making a solution automatically makes the language it depends on, that a whole-project - * make succeeds, that a module whose closure reaches jar-packaged library sources still makes, and that a broken model - * fails with reported errors. + * make succeeds, that a module whose closure reaches jar-packaged library sources still makes, that a broken model + * fails with reported errors, and that modules and models with nothing to generate are left out of the make. */ class MakeSemanticsTest { @Test @@ -108,6 +122,62 @@ class MakeSemanticsTest { model.writeText(broken) } + // The tests below send each step as its own daemon request, as the CLI does. + @Test + fun `a solution with no generation target facets makes without generating its models`() { + val (response, solutionDir) = SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> + val daemon = DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) + createSolutionWithModel(daemon, "example.analyzed", SolutionUsagePreset.NOT_GENERATED) + val addClass = EditOperation.AddRoot( + model = ModelDestination("example.analyzed.main"), + concept = CLASS_CONCEPT, + properties = listOf(MpsNodePropertyJson(name = "name", value = "Analyzed")), + ) + daemon.handleDomainRequest(ModelEditRequest("", EditBatch(listOf(addClass)))) + val generation = MakeGenerationOptions(messages = MakeMessageLevel.INFO) + daemon.make("example.analyzed", generation) to projectPath.resolve("solutions/example.analyzed") + } + + assertEquals(MakeOutcome.NOTHING_TO_GENERATE, response.outcome, "got ${response.messages}") + assertTrue( + response.messages.any { it.kind == MakeMessageKind.INFO && it.text.contains("example.analyzed") }, + "expected an info message naming the module left out; got ${response.messages}", + ) + assertFalse(solutionDir.resolve("source_gen").exists(), "the solution's models must not be generated") + } + + @Test + fun `a solution with no generation target facets and an empty model has nothing to generate`() { + val response = SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> + val daemon = DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) + createSolutionWithModel(daemon, "example.analyzed", SolutionUsagePreset.NOT_GENERATED) + daemon.make("example.analyzed") + } + + assertEquals(MakeOutcome.NOTHING_TO_GENERATE, response.outcome, "got ${response.messages}") + } + + @Test + fun `a generated solution whose only model is empty has nothing to generate`() { + val response = SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> + val daemon = DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) + createSolutionWithModel(daemon, "example.empty", SolutionUsagePreset.JAVA) + daemon.make("example.empty") + } + + assertEquals(MakeOutcome.NOTHING_TO_GENERATE, response.outcome, "got ${response.messages}") + } + + private fun createSolutionWithModel(daemon: DomainRequestHandler, name: String, preset: SolutionUsagePreset) { + daemon.handleDomainRequest(CreateSolutionRequest("", name, usagePreset = preset)) + daemon.handleDomainRequest(CreateModelRequest("", "$name.main", name)) + } + + private fun DomainRequestHandler.make( + module: String, + generation: MakeGenerationOptions = MakeGenerationOptions(), + ): MakeResponse = assertIs(handleDomainRequest(MakeModulesRequest("", listOf(module), generation))) + @Test fun `an unknown module name fails with a target-not-found error`() { val failure = assertFailsWith { @@ -122,5 +192,6 @@ class MakeSemanticsTest { private companion object { const val SANDBOX = "base-language-sandbox" const val EXPRESSION_LANGUAGE = "expression-language" + const val CLASS_CONCEPT = "jetbrains.mps.baseLanguage.structure.ClassConcept" } } diff --git a/docs/reference/make.md b/docs/reference/make.md index 39dbf766..efa042db 100644 --- a/docs/reference/make.md +++ b/docs/reference/make.md @@ -54,7 +54,9 @@ module(s): success`, `made module(s): FAILED ( error(s))`, or `nothing to generate`. The command exits non-zero when the make reports errors. The make generates every model of the made modules and deletes files in their -output folders that it no longer generates. +output folders that it no longer generates. It leaves out the models of modules +without a generation target facet, such as solutions created with the +`not-generated` usage preset; `--messages info` names these modules. The generation options apply to this make only; without them the make uses the daemon's MPS generation settings. With `--threads`, MPS generates in parallel @@ -100,7 +102,9 @@ module(s): success`, `made module(s): FAILED ( error(s))`, or `nothing to generate`. The command exits non-zero when the make reports errors. The make generates every model of the made modules and deletes files in their -output folders that it no longer generates. +output folders that it no longer generates. It leaves out the models of modules +without a generation target facet, such as solutions created with the +`not-generated` usage preset; `--messages info` names these modules. The generation options apply to this make only; without them the make uses the daemon's MPS generation settings. With `--threads`, MPS generates in parallel diff --git a/examples/pages/editing.creation.md b/examples/pages/editing.creation.md index e715c4e7..bed08446 100644 --- a/examples/pages/editing.creation.md +++ b/examples/pages/editing.creation.md @@ -3,7 +3,7 @@ {{table:commands|Command}} Solution presets configure facets, not dependencies, imported languages, devkits, or contents. Supported presets: -`not-generated` (default, no facets), `text` (plain-text generation), `java` (Java compiled and loaded by MPS), -`java-tests` (`java` plus the tests facet), and `java-mps-plugin` (Java extending MPS as a plugin). The creation report -and the `--dry-run` plan print the preset applied and the facets it produced; `--json` carries them as `usagePreset` and -`facets`. +`not-generated` (default, no facets, so make leaves its models out), `text` (plain-text generation), `java` (Java +compiled and loaded by MPS), `java-tests` (`java` plus the tests facet), and `java-mps-plugin` (Java extending MPS as a +plugin). The creation report and the `--dry-run` plan print the preset applied and the facets it produced; `--json` +carries them as `usagePreset` and `facets`. diff --git a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt index ba4d23e4..315d2acc 100644 --- a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt +++ b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt @@ -13,9 +13,15 @@ enum class MakeOutcome { /** The make finished and reported no errors. */ SUCCESS, - /** The make result was a failure, or at least one error message was reported. */ + /** + * The make result was a failure, or at least one error message was reported. A failed make always carries at least + * one error message. + */ FAILED, - /** The make set held no generatable model, so nothing ran. */ + /** + * The make set held no model to generate, so nothing ran: no model is generatable, its module has no generation + * target facet, or it uses no language. + */ NOTHING_TO_GENERATE, } diff --git a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt index 090e6de9..269aa94e 100644 --- a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt +++ b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt @@ -183,8 +183,8 @@ data class ModuleDiagnosticResponse( /** * Successful response carrying the result of a make run: its [outcome], how many generatable modules were in the make * set (the requested modules plus their generatable dependency closure), and the error and warning [messages] MPS - * reported. On [MakeOutcome.FAILED] the [messages] carry the errors; on [MakeOutcome.NOTHING_TO_GENERATE] the make set - * held no generatable model. + * reported. On [MakeOutcome.FAILED] the [messages] carry at least one error; on [MakeOutcome.NOTHING_TO_GENERATE] the + * make set held no model to generate. */ @Serializable @SerialName("make-result") diff --git a/skills/mops/references/editing.creation.md b/skills/mops/references/editing.creation.md index 7abdf7af..e308061e 100644 --- a/skills/mops/references/editing.creation.md +++ b/skills/mops/references/editing.creation.md @@ -13,7 +13,7 @@ | Preview creation | `mops create solution sample.solution --usage-preset java --dry-run` | Solution presets configure facets, not dependencies, imported languages, devkits, or contents. Supported presets: -`not-generated` (default, no facets), `text` (plain-text generation), `java` (Java compiled and loaded by MPS), -`java-tests` (`java` plus the tests facet), and `java-mps-plugin` (Java extending MPS as a plugin). The creation report -and the `--dry-run` plan print the preset applied and the facets it produced; `--json` carries them as `usagePreset` and -`facets`. +`not-generated` (default, no facets, so make leaves its models out), `text` (plain-text generation), `java` (Java +compiled and loaded by MPS), `java-tests` (`java` plus the tests facet), and `java-mps-plugin` (Java extending MPS as a +plugin). The creation report and the `--dry-run` plan print the preset applied and the facets it produced; `--json` +carries them as `usagePreset` and `facets`. From edcb0460555d795fe1d625008161579a2bdfa19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 20:08:01 +0100 Subject: [PATCH 2/2] fix(make): report left-out modules as warnings and tidy the make docs --- .../mops/cli/MakeIntegrationTest.kt | 4 +-- .../mops/cli/make/makeHelp.kt | 6 ++-- .../mops/daemon/ProjectMake.kt | 31 +++++++++---------- .../mops/daemon/MakeSemanticsTest.kt | 12 +++---- docs/reference/make.md | 8 ++--- .../mops/protocol/MakeOutcome.kt | 5 +-- .../mops/protocol/daemonResponses.kt | 6 ++-- 7 files changed, 33 insertions(+), 39 deletions(-) diff --git a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt index 4893bcb6..dc9f3f71 100644 --- a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt +++ b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt @@ -38,11 +38,11 @@ class MakeIntegrationTest { val model = project.run("create", "model", "example.analyzed.main", "--module", "example.analyzed") assertEquals(0, model.exitCode, model.output) - val result = project.run("make", "module", "--messages", "info", "example.analyzed") + val result = project.run("make", "module", "example.analyzed") assertEquals(0, result.exitCode, result.output) assertEquals("nothing to generate", result.stdout.trim(), result.output) - assertContains(result.stderr, "info\tnot generating the models of example.analyzed") + assertContains(result.stderr, "warning\tnot generating the models of example.analyzed") } companion object { diff --git a/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt b/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt index b343dd71..04aa83fd 100644 --- a/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt +++ b/cli/src/main/kotlin/com/specificlanguages/mops/cli/make/makeHelp.kt @@ -6,11 +6,11 @@ internal const val MAKE_OUTPUT_HELP = "a one-line action report on stdout: `made module(s): success`, `made module(s): FAILED ( " + "error(s))`, or `nothing to generate`. The command exits non-zero when the make reports errors." -/** What a make deletes, for the help footers of both make commands. */ +/** What a make generates and deletes, for the help footers of both make commands. */ internal const val MAKE_CLEAN_HELP = "The make generates every model of the made modules and deletes files in their output folders that it no " + - "longer generates. It leaves out the models of modules without a generation target facet, such as " + - "solutions created with the `not-generated` usage preset; `--messages info` names these modules." + "longer generates. It leaves out the models of modules without generation-target facets, such as " + + "solutions created with the `not-generated` usage preset, and names each such module in a warning." /** How the generation options behave, for the help footers of both make commands. */ internal const val MAKE_GENERATION_HELP = diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt index 2cee5c9b..3aada442 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ProjectMake.kt @@ -19,7 +19,6 @@ import jetbrains.mps.make.script.IScriptController import jetbrains.mps.make.service.CoreMakeTask import jetbrains.mps.messages.IMessage import jetbrains.mps.messages.IMessageHandler -import jetbrains.mps.messages.Message import jetbrains.mps.messages.MessageKind import jetbrains.mps.progress.EmptyProgressMonitor import jetbrains.mps.project.AbstractModule @@ -41,8 +40,8 @@ import org.jetbrains.mps.openapi.util.ProgressMonitor * The make framework builds exactly the modules whose models it is handed — it never pulls in a dependency that is not * in the set, it only orders what it is given. So to make un-made dependencies too, [makeModules] first expands the * requested modules to their transitive compile-dependency closure ([GlobalModuleDependenciesManager]); already-built - * library dependencies drop out because [ModelsToResources] keeps only generatable models. Models of modules without a - * generation target facet are left out too ([makeSet]). Every model handed to the make is generated; nothing skips + * library dependencies drop out because [ModelsToResources] keeps only generatable models. Models of modules without + * generation-target facets are left out too ([makeSet]). Every model handed to the make is generated; nothing skips * unmodified models. * * Model collection runs inside short read actions; the make call runs outside any model action, on the calling thread, @@ -103,7 +102,9 @@ class ProjectMake(private val project: Project) { val options = makeGenerationOptions(generationSettings, generation) val makeSet = project.modelAccess.computeReadAction { makeSet(models) } val handler = CollectingMessageHandler() - makeSet.leftOutModules.forEach { handler.handle(Message(MessageKind.INFORMATION, it.notGeneratedMessage())) } + makeSet.leftOutModules.mapTo(handler.messages) { + MakeMessageJson(MakeMessageKind.WARNING, it.notGeneratedMessage()) + } // MakeSession's own clean flag only names the IDE's make script; the resources above carry the clean make. val session = MakeSession(project, handler, /* cleanMake = */ false) @@ -111,11 +112,7 @@ class ProjectMake(private val project: Project) { // can have no step even when there are resources. val sequence = MakeSequence(makeSet.resources, null, session) if (sequence.steps() == 0) { - return MakeResponse( - MakeOutcome.NOTHING_TO_GENERATE, - moduleCount = 0, - messages = handler.messages.filter { generation.messages.reports(it.kind) }, - ) + return MakeResponse(MakeOutcome.NOTHING_TO_GENERATE, 0, handler.messages.reportedAt(generation.messages)) } // Reimplements BuildMakeService.doMake using classes from mps-core: BuildMakeService itself ships only in @@ -132,13 +129,13 @@ class ProjectMake(private val project: Project) { } /** - * The make resources for [models], grouped one per module, and the modules whose models are left out because they - * have nowhere to write generated files. Must run inside a read action. + * The make resources for [models], grouped one per module, and the modules whose generatable models are left out + * because they have nowhere to write generated files. Must run inside a read action. * * Models of read-only modules are left out: MPS ships its library languages and runtime solutions as *source* * models inside `-src.jar` files, and those models report themselves generatable, yet writing their generation - * output into the jar fails ("Write for jar files is not supported"). Models whose module has no generation target - * facet are left out too: text generation has no output location for them, which fails the make on MPS before + * output into the jar fails ("Write for jar files is not supported"). Models whose module has no generation-target + * facets are left out too: text generation has no output location for them, which fails the make on MPS before * 2025.3. See https://github.com/specificlanguages/mps-api-research/blob/main/make-generation.md. */ private fun makeSet(models: List): MakeSet { @@ -153,7 +150,7 @@ class ProjectMake(private val project: Project) { private class MakeSet(val resources: List, val leftOutModules: List) private fun SModule.notGeneratedMessage(): String = - "not generating the models of $moduleName: the module has no generation target facet" + "not generating the models of $moduleName: the module has no generation-target facets" /** * The modules to make for a request over [requested]: the requested modules, their module-level compile-dependency @@ -242,7 +239,7 @@ internal fun makeResponse( ): MakeResponse { val hasError = messages.any { it.kind == MakeMessageKind.ERROR } val succeeded = result != null && result.isSucessful && !hasError - val reported = messages.filter { level.reports(it.kind) } + val reported = messages.reportedAt(level) return MakeResponse( outcome = if (succeeded) MakeOutcome.SUCCESS else MakeOutcome.FAILED, moduleCount = moduleCount, @@ -250,6 +247,9 @@ internal fun makeResponse( ) } +private fun List.reportedAt(level: MakeMessageLevel?): List = + filter { level.reports(it.kind) } + private fun unexplainedFailure(result: IResult?): MakeMessageJson { val reason = result?.message().orEmpty().ifBlank { null } val text = if (reason == null) { @@ -259,4 +259,3 @@ private fun unexplainedFailure(result: IResult?): MakeMessageJson { } return MakeMessageJson(MakeMessageKind.ERROR, text) } - diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt index 2086c2a8..ce0c3b06 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/MakeSemanticsTest.kt @@ -7,7 +7,6 @@ import com.specificlanguages.mops.protocol.CreateSolutionRequest import com.specificlanguages.mops.protocol.EditBatch import com.specificlanguages.mops.protocol.EditOperation import com.specificlanguages.mops.protocol.MakeGenerationOptions -import com.specificlanguages.mops.protocol.MakeMessageLevel import com.specificlanguages.mops.protocol.MakeModulesRequest import com.specificlanguages.mops.protocol.MakeMessageKind import com.specificlanguages.mops.protocol.MakeOutcome @@ -124,7 +123,7 @@ class MakeSemanticsTest { // The tests below send each step as its own daemon request, as the CLI does. @Test - fun `a solution with no generation target facets makes without generating its models`() { + fun `a solution with no generation-target facets makes without generating its models`() { val (response, solutionDir) = SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> val daemon = DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) createSolutionWithModel(daemon, "example.analyzed", SolutionUsagePreset.NOT_GENERATED) @@ -134,20 +133,19 @@ class MakeSemanticsTest { properties = listOf(MpsNodePropertyJson(name = "name", value = "Analyzed")), ) daemon.handleDomainRequest(ModelEditRequest("", EditBatch(listOf(addClass)))) - val generation = MakeGenerationOptions(messages = MakeMessageLevel.INFO) - daemon.make("example.analyzed", generation) to projectPath.resolve("solutions/example.analyzed") + daemon.make("example.analyzed") to projectPath.resolve("solutions/example.analyzed") } assertEquals(MakeOutcome.NOTHING_TO_GENERATE, response.outcome, "got ${response.messages}") assertTrue( - response.messages.any { it.kind == MakeMessageKind.INFO && it.text.contains("example.analyzed") }, - "expected an info message naming the module left out; got ${response.messages}", + response.messages.any { it.kind == MakeMessageKind.WARNING && it.text.contains("example.analyzed") }, + "expected a warning naming the module left out; got ${response.messages}", ) assertFalse(solutionDir.resolve("source_gen").exists(), "the solution's models must not be generated") } @Test - fun `a solution with no generation target facets and an empty model has nothing to generate`() { + fun `a solution with no generation-target facets and an empty model has nothing to generate`() { val response = SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> val daemon = DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) createSolutionWithModel(daemon, "example.analyzed", SolutionUsagePreset.NOT_GENERATED) diff --git a/docs/reference/make.md b/docs/reference/make.md index efa042db..890e23b1 100644 --- a/docs/reference/make.md +++ b/docs/reference/make.md @@ -55,8 +55,8 @@ to generate`. The command exits non-zero when the make reports errors. The make generates every model of the made modules and deletes files in their output folders that it no longer generates. It leaves out the models of modules -without a generation target facet, such as solutions created with the -`not-generated` usage preset; `--messages info` names these modules. +without generation-target facets, such as solutions created with the +`not-generated` usage preset, and names each such module in a warning. The generation options apply to this make only; without them the make uses the daemon's MPS generation settings. With `--threads`, MPS generates in parallel @@ -103,8 +103,8 @@ to generate`. The command exits non-zero when the make reports errors. The make generates every model of the made modules and deletes files in their output folders that it no longer generates. It leaves out the models of modules -without a generation target facet, such as solutions created with the -`not-generated` usage preset; `--messages info` names these modules. +without generation-target facets, such as solutions created with the +`not-generated` usage preset, and names each such module in a warning. The generation options apply to this make only; without them the make uses the daemon's MPS generation settings. With `--threads`, MPS generates in parallel diff --git a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt index 315d2acc..16e578d1 100644 --- a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt +++ b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/MakeOutcome.kt @@ -19,9 +19,6 @@ enum class MakeOutcome { */ FAILED, - /** - * The make set held no model to generate, so nothing ran: no model is generatable, its module has no generation - * target facet, or it uses no language. - */ + /** The make set held no model to generate, so nothing ran. */ NOTHING_TO_GENERATE, } diff --git a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt index 269aa94e..82e6c1ba 100644 --- a/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt +++ b/protocol/src/main/kotlin/com/specificlanguages/mops/protocol/daemonResponses.kt @@ -182,9 +182,9 @@ data class ModuleDiagnosticResponse( /** * Successful response carrying the result of a make run: its [outcome], how many generatable modules were in the make - * set (the requested modules plus their generatable dependency closure), and the error and warning [messages] MPS - * reported. On [MakeOutcome.FAILED] the [messages] carry at least one error; on [MakeOutcome.NOTHING_TO_GENERATE] the - * make set held no model to generate. + * set (the requested modules plus their generatable dependency closure), and the make [messages] at the requested level. + * On [MakeOutcome.FAILED] the [messages] carry at least one error; on [MakeOutcome.NOTHING_TO_GENERATE] the make set + * held no model to generate. */ @Serializable @SerialName("make-result")