diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9b633d56ba..35e56d7f07 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2178[#2178]: Make ReleaseCommandlet independent of specific build commandlet and fix `ide build` using npm instead of yarn * https://github.com/devonfw/IDEasy/issues/989[#989]: Allow expressions in template variable definitions The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java index 99c7c131de..f47cf8f088 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java @@ -8,18 +8,12 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.tool.LocalToolCommandlet; -import com.devonfw.tools.ide.tool.gradle.Gradle; -import com.devonfw.tools.ide.tool.mvn.Mvn; -import com.devonfw.tools.ide.tool.npm.Npm; -import com.devonfw.tools.ide.tool.yarn.Yarn; /** * Build tool {@link Commandlet} for automatically detecting build configuration files and running the respective tool. */ public class BuildCommandlet extends Commandlet { - private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); - /** The explicit build options to use (if empty use defaults). */ public final StringProperty arguments; @@ -50,21 +44,14 @@ protected void doRun() { throw new CliException("Missing current working directory!"); } - List args = this.arguments.asList(); - LocalToolCommandlet commandlet = null; - for (Class toolClass : BUILD_TOOLS) { - LocalToolCommandlet toolCommandlet = this.context.getCommandletManager().getCommandlet(toolClass); - Path buildDescriptor = toolCommandlet.findBuildDescriptor(buildPath); - if (buildDescriptor != null) { - commandlet = toolCommandlet; - if (args.isEmpty()) { - String variableName = commandlet.getName().toUpperCase(Locale.ROOT) + "_BUILD_OPTS"; - args = getDefaultToolOptions(variableName); - } - } - } + LocalToolCommandlet commandlet = this.context.getCommandletManager().findBuildTool(buildPath); if (commandlet == null) { - throw new CliException("Could not find build descriptor - no pom.xml, build.gradle, or package.json found!"); + throw new CliException("Could not find a build descriptor in " + buildPath + " - no supported build tool detected."); + } + List args = this.arguments.asList(); + if (args.isEmpty()) { + String variableName = commandlet.getName().toUpperCase(Locale.ROOT) + "_BUILD_OPTS"; + args = getDefaultToolOptions(variableName); } commandlet.runTool(args); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java index 92105a33ea..dd6cc327f8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Path; import java.util.Collection; import java.util.Iterator; @@ -106,4 +107,13 @@ default LocalToolCommandlet getRequiredLocalToolCommandlet(String name) { */ Iterator findCommandlet(CliArguments arguments, CompletionCandidateCollector collector); + /** + * Detects the applicable build tool for the given {@code buildPath} by {@link LocalToolCommandlet#findBuildDescriptor(Path) querying} the registered build + * commandlets (in order of priority) for a matching build descriptor (e.g. {@code pom.xml}, {@code build.gradle} or {@code package.json}). + * + * @param buildPath the {@link Path} to the directory to build. + * @return the applicable build {@link LocalToolCommandlet} or {@code null} if no build descriptor was found or {@code buildPath} was {@code null}. + */ + LocalToolCommandlet findBuildTool(Path buildPath); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index f98a63decd..c42d462bb8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -19,6 +20,7 @@ import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; import com.devonfw.tools.ide.property.KeywordProperty; import com.devonfw.tools.ide.property.Property; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.tool.androidstudio.AndroidStudio; import com.devonfw.tools.ide.tool.aws.Aws; import com.devonfw.tools.ide.tool.az.Azure; @@ -82,6 +84,9 @@ public class CommandletManagerImpl implements CommandletManager { private static final Logger LOG = LoggerFactory.getLogger(CommandletManagerImpl.class); + /** The build commandlets in order of priority - the first one with a matching build descriptor wins. */ + private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); + private final IdeContext context; private final Map, Commandlet> commandletTypeMap; @@ -279,6 +284,21 @@ public Iterator findCommandlet(CliArguments arguments, CompletionCan return new CommandletFinder(commandlet, arguments.copy(), collector); } + @Override + public LocalToolCommandlet findBuildTool(Path buildPath) { + + if (buildPath == null) { + return null; + } + for (Class toolClass : BUILD_TOOLS) { + LocalToolCommandlet toolCommandlet = getCommandlet(toolClass); + if (toolCommandlet.findBuildDescriptor(buildPath) != null) { + return toolCommandlet; + } + } + return null; + } + private final class CommandletFinder implements Iterator { private final Commandlet firstCandidate; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index 141992e9fa..2c14eff032 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.commandlet; -import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; @@ -11,7 +10,8 @@ import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.process.ProcessResult; import com.devonfw.tools.ide.property.StringProperty; -import com.devonfw.tools.ide.tool.mvn.Mvn; +import com.devonfw.tools.ide.tool.BuildTool; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.version.VersionIdentifier; /** @@ -41,15 +41,23 @@ protected void doRun() { Path projectPath = this.context.getCwd(); GitContext git = this.context.getGitContext(); - Mvn buildTool = this.context.getCommandletManager().getCommandlet(Mvn.class); + + LocalToolCommandlet commandlet = this.context.getCommandletManager().findBuildTool(projectPath); + if (commandlet == null) { + throw new CliException("Could not find a build descriptor in " + projectPath + ". There is nothing to release here."); + } + if (!(commandlet instanceof BuildTool buildTool)) { + throw new CliException("The build tool " + commandlet.getName() + " detected in " + projectPath + " does not support releasing."); + } if (git.hasUntrackedFiles(projectPath)) { throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on clean repo."); } if (warnIfFork(git, projectPath)) { - confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\nWe strongly recommend to abort and rerun on original repository."); + confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\n" + + "We strongly recommend to abort and rerun on original repository."); } - if (!this.context.isForceMode() && !isTopLevelProject(projectPath)) { + if (!this.context.isForceMode() && !isTopLevelProject(commandlet, projectPath)) { throw new CliException("Release has to be performed from the top-level project or using force option."); } @@ -94,14 +102,14 @@ private boolean warnIfFork(GitContext git, Path projectPath) { return false; } - private boolean isTopLevelProject(Path projectPath) { + private boolean isTopLevelProject(LocalToolCommandlet buildCommandlet, Path projectPath) { - // returns false in case there's no pom.xml present or if parent directory has a pom.xml - return Files.exists(projectPath.resolve("pom.xml")) - && !Files.exists(projectPath.getParent().resolve("pom.xml")); + // top-level if the build descriptor found here is not also present in the parent directory + Path parent = projectPath.getParent(); + return (parent == null) || (buildCommandlet.findBuildDescriptor(parent) == null); } - private void buildAndDeploy(Mvn buildTool) { + private void buildAndDeploy(BuildTool buildTool) { while (true) { ProcessResult result = buildTool.buildAndDeploy(this.arguments.asList()); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java index 9bda384fac..a6c5ae4962 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java @@ -2,6 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; import com.devonfw.tools.ide.cli.CliException; @@ -11,6 +13,10 @@ import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.os.SystemInfo; import com.devonfw.tools.ide.os.SystemInfoMock; +import com.devonfw.tools.ide.tool.gradle.Gradle; +import com.devonfw.tools.ide.tool.mvn.Mvn; +import com.devonfw.tools.ide.tool.npm.Npm; +import com.devonfw.tools.ide.tool.yarn.Yarn; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; @@ -118,4 +124,24 @@ void testBuildWithNoBuildFile() { context.setCwd(context.getWorkspacePath().resolve("empty"), context.getWorkspacePath().toString(), context.getIdeHome()); assertThrows(CliException.class, buildCommandlet::run); } + + /** + * Tests {@link CommandletManager#findBuildTool(Path)} detecting the applicable build tool by its build + * descriptor and preferring {@link Yarn} over {@link Npm} when a {@code yarn.lock} is present. + */ + @Test + void testFindBuildTool() { + + IdeTestContext context = newContext(PROJECT_BUILD); + Path workspace = context.getWorkspacePath(); + + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("mvn"))).isInstanceOf(Mvn.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("gradle"))).isInstanceOf(Gradle.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("npm"))).isInstanceOf(Npm.class); + // both npm and yarn match package.json, but yarn.lock is present so yarn must take precedence over npm + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("yarn"))).isInstanceOf(Yarn.class); + // a polyglot project must be built by the highest-priority tool, not the last one that matches + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("mvn-and-npm"))).isInstanceOf(Mvn.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("empty"))).isNull(); + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java index a17e2db7e0..4905037d8a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java @@ -91,6 +91,22 @@ void testReleaseWithoutBuildDescriptor() { context.setCwd(context.getWorkspacePath().resolve("empty"), context.getWorkspacePath().toString(), context.getIdeHome()); ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); - assertThrows(CliException.class, releaseCommandlet::run); + CliException exception = assertThrows(CliException.class, releaseCommandlet::run); + assertThat(exception).hasMessageContaining("Could not find a build descriptor"); + } + + /** + * Tests that the release fails gracefully if a build descriptor is found but its build tool does not support releasing (does not implement + * {@link com.devonfw.tools.ide.tool.BuildTool}), e.g. a gradle project (only maven currently supports releasing). + */ + @Test + void testReleaseWithUnsupportedBuildToolThrowsException() { + + IdeTestContext context = newReleaseContext(false); + context.setCwd(context.getWorkspacePath().resolve("gradle"), context.getWorkspacePath().toString(), context.getIdeHome()); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + + CliException exception = assertThrows(CliException.class, releaseCommandlet::run); + assertThat(exception).hasMessageContaining("gradle").hasMessageContaining("does not support releasing"); } } diff --git a/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/package.json b/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/package.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/pom.xml b/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/pom.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/test/resources/ide-projects/release/project/workspaces/main/gradle/build.gradle b/cli/src/test/resources/ide-projects/release/project/workspaces/main/gradle/build.gradle new file mode 100644 index 0000000000..e69de29bb2