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
1 change: 1 addition & 0 deletions .devpack-for-spring/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
devpack-for-spring-cli
617 changes: 617 additions & 0 deletions .devpack-for-spring/plugin-configuration.yaml

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion .github/workflows/pr-self-hosted.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ jobs:
rm -rf /home/runner/.local/state/rockcraft/log/*
sudo snap install rockcraft --classic
- run: |
export LOGGING_LEVEL_COM_CANONICAL_DEVPACKSPRING_PROCESSUTIL=DEBUG
./gradlew build -PexcludeTags=boot,maven-modification,maven-dependency --no-daemon --stacktrace
- name: Store reports
if: failure()
Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,7 @@ if (useAot) {
}

}

bootJar {
requiresUnpack '**/kotlin-compiler-embeddable-*.jar'
}
33 changes: 19 additions & 14 deletions src/main/java/com/canonical/devpackspring/ConfigUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,31 @@ public abstract class ConfigUtil {
private static final Log LOG = LogFactory.getLog(ConfigUtil.class);

/**
* Opens configuration file stream
* Opens configuration file stream from: 1. System Property 2. Environment Variable 3.
* Current directory .devpack-for-spring/conffile 4. User home
* .config/devpack-for-spring/conffile 5. Embedded resource
* /com/canonical/devpackspring/conffile
* @param environment - environment variable specifying path to the file
* @param fileName - configuration file name
* @return configuration file InputStream
* @throws FileNotFoundException - configuration file not found
*/
public static InputStream openConfigurationFile(String environment, String fileName) throws FileNotFoundException {

String pluginConfigurationFile = System.getProperty(environment);
if (pluginConfigurationFile == null) {
pluginConfigurationFile = System.getenv(environment);
}
if (pluginConfigurationFile != null) {
if (Files.exists(Path.of(pluginConfigurationFile))) {
LOG.info("Reading configuration from " + pluginConfigurationFile);
return new FileInputStream(pluginConfigurationFile);
}
else {
LOG.warn("Configuration file " + environment + "=" + pluginConfigurationFile + " does not exist.");
}
}

Path currentConfigPath = Path.of(System.getProperty("user.dir"))
.resolve(".devpack-for-spring")
.resolve(fileName);
Expand All @@ -57,19 +75,6 @@ public static InputStream openConfigurationFile(String environment, String fileN
return new FileInputStream(configPath.toFile());
}

String pluginConfigurationFile = System.getenv(environment);
if (pluginConfigurationFile == null) {
pluginConfigurationFile = System.getProperty(environment);
}
if (pluginConfigurationFile != null) {
if (Files.exists(Path.of(pluginConfigurationFile))) {
LOG.info("Reading configuration from " + pluginConfigurationFile);
return new FileInputStream(pluginConfigurationFile);
}
else {
LOG.warn("Configuration file " + environment + "=" + pluginConfigurationFile + " does not exist.");
}
}
LOG.info("Reading default configuration " + fileName);
return ConfigUtil.class.getResourceAsStream(String.format("/com/canonical/devpackspring/%s", fileName));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ private boolean addOrReplace(List<Statement> targetStatements, Statement configS
for (int i = 0; i < targetStatements.size(); i++) {
Statement targetStmt = targetStatements.get(i);
if (matches(targetStmt, configStmt)) {
// Special case: merge dependencies{} blocks instead of replacing
if (isDependenciesBlock(targetStmt)) {
Statement merged = DependencyMergeUtil.mergeDependenciesBlock(targetStmt, configStmt, targetCu,
configCu);
if (merged != null) {
targetStatements.set(i, merged);
return true;
}
return false;
}
// avoid modifying if the trimmed outputs are strictly identical
org.openrewrite.Cursor targetCursor = new org.openrewrite.Cursor(
new org.openrewrite.Cursor(null, targetCu), targetStmt);
Expand All @@ -142,6 +152,10 @@ private boolean addOrReplace(List<Statement> targetStatements, Statement configS
return true;
}

private boolean isDependenciesBlock(Statement stmt) {
return stmt instanceof J.MethodInvocation m && "dependencies".equals(m.getSimpleName());
}

private boolean matches(Statement targetStmt, Statement configStmt) {
// handle project.ext.set -> we want to override only properties with the same
// name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.canonical.devpackspring.rewrite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import org.openrewrite.Cursor;
import org.openrewrite.SourceFile;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Statement;

public abstract class DependencyMergeUtil {

public static Statement mergeDependenciesBlock(Statement targetStmt, Statement configStmt, SourceFile targetCu,
SourceFile configCu) {
J.MethodInvocation targetMethod = (J.MethodInvocation) targetStmt;
J.MethodInvocation configMethod = (J.MethodInvocation) configStmt;

List<Statement> targetDeps = getDependenciesStatements(targetMethod);
List<Statement> configDeps = getDependenciesStatements(configMethod);
if (targetDeps == null) {
return configStmt;
}
if (configDeps == null) {
return null; // nothing to merge, skip it
}

HashSet<String> deps = new HashSet<>();
HashMap<String, Statement> target = new HashMap<>();
HashMap<String, Statement> source = new HashMap<>();
targetDeps.forEach(x -> updateDependencies(x, deps, target));
configDeps.forEach(x -> updateDependencies(x, deps, source));

List<Statement> mergedDeps = new ArrayList<>();
var sortedList = new ArrayList<>(deps);
Collections.sort(sortedList);
for (var key : sortedList) {
Statement stm = source.get(key);
if (stm != null) {
if (!mergedDeps.isEmpty()) {
stm = stm.withPrefix(mergedDeps.getLast().getPrefix());
}
mergedDeps.add(stm);
continue;
}
stm = target.get(key);
if (stm != null) {
if (!mergedDeps.isEmpty()) {
stm = stm.withPrefix(mergedDeps.getLast().getPrefix());
}
mergedDeps.add(stm);
}
}
Statement rebuilt = rebuildDependenciesStatements(targetMethod, mergedDeps);
if (rebuilt == null) {
return null;
}
Cursor rc = new Cursor(new Cursor(null, targetCu), rebuilt);
String newResult = rebuilt.printTrimmed(rc).trim();
Cursor cc = new Cursor(new Cursor(null, configCu), targetStmt);
String oldResult = targetStmt.printTrimmed(cc).trim();
if (oldResult.equals(newResult)) {
return null;
}
return rebuilt;
}

private static void updateDependencies(Statement stm, HashSet<String> deps, HashMap<String, Statement> depMap) {
var key = getDependencyKey(stm);
if (key == null) {
throw new RuntimeException("Unexpected element in depends block " + stm.toString());
}
deps.add(key);
depMap.put(key, stm);
}

private static List<Statement> getDependenciesStatements(J.MethodInvocation m) {
if (m.getArguments().size() == 1) {
org.openrewrite.java.tree.Expression arg = m.getArguments().getFirst();
if (arg instanceof J.Lambda lambda && lambda.getBody() instanceof J.Block block) {
return block.getStatements().stream().map(stmt -> {
if (stmt instanceof J.Return ret) {
return (Statement) ret.getExpression();
}
return stmt;
}).toList();
}
}
return null;
}

private static Statement rebuildDependenciesStatements(J.MethodInvocation m, List<Statement> newStatements) {
if (m.getArguments().size() == 1) {
org.openrewrite.java.tree.Expression arg = m.getArguments().getFirst();
if (arg instanceof J.Lambda lambda && lambda.getBody() instanceof J.Block block) {
J.Block newBlock = block.withStatements(newStatements.stream().map(stmt -> {
String ws = stmt.getPrefix().getWhitespace();
if (!ws.contains("\n")) {
stmt = stmt.withPrefix(stmt.getPrefix().withWhitespace("\n" + ws));
}
return stmt;
}).toList());
J.Lambda newLambda = lambda.withBody(newBlock);
return m.withArguments(List.of(newLambda));
}
}
return null;
}

private static String getDependencyKey(Statement stmt) {
if (!(stmt instanceof J.MethodInvocation m)) {
return null;
}
String scope = m.getSimpleName();
if (m.getArguments().isEmpty()) {
return null;
}
String coord = m.getArguments().getFirst().toString();
// Strip the version segment: "group:artifact:version" -> "group:artifact"
String[] parts = coord.split(":");
if (parts.length >= 2) {
return scope + ":" + parts[0] + ":" + parts[1];
}
if (parts.length == 1) {
return scope + ":" + coord;
}
throw new RuntimeException("Unexpected statement " + stmt);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.List;

import com.canonical.devpackspring.rewrite.StatementUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
Expand All @@ -31,9 +33,12 @@
import org.openrewrite.kotlin.KotlinIsoVisitor;
import org.openrewrite.kotlin.KotlinParser;
import org.openrewrite.kotlin.tree.K;
import org.openrewrite.tree.ParseError;

public class KotlinAddPluginVisitor extends KotlinIsoVisitor<ExecutionContext> {

private static final Log LOG = LogFactory.getLog(KotlinAddPluginVisitor.class);

private final String pluginTemplateKotlin = "plugins {\n\tid(\"%s\") version \"%s\"\n}\n";

private final String builtInTemplateKotlin = "plugins {\n\tid(\"%s\")\n}\n";
Expand All @@ -56,7 +61,10 @@ public KotlinAddPluginVisitor(String pluginName, String pluginVersion) {
Paths.get("/tmp"), context)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Could not parse as Gradle Kotlin"));

if (templateSource instanceof ParseError error) {
LOG.error("Unable to parse: " + pluginDefinition);
throw new RuntimeException("Parser Error:" + error.printAll());
}
List<Statement> statements = ((K.CompilationUnit) templateSource).getStatements();
J.Block block = (J.Block) statements.get(0);
K.MethodInvocation stm = (K.MethodInvocation) block.getStatements().get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,92 @@ void testKotlinConfigurationAppendProperty() {
"""));
}

@Test
void testGroovyDependenciesMerge() {
String config = """
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.5.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}
""";
G.CompilationUnit cu = parseGroovyConfig(config);
rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, false)), Assertions.buildGradle("""
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.3.0'
runtimeOnly 'org.postgresql:postgresql:42.7.0'
}
""", """
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.5.0'
runtimeOnly 'org.postgresql:postgresql:42.7.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}
"""));
rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, false)), Assertions.buildGradle("""
dependencies {
}
""", """
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.5.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}
"""));
}

@Test
void testGroovyDependenciesMergeNewLines() {
String config = """
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}
""";
G.CompilationUnit cu = parseGroovyConfig(config);
rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, false)), Assertions.buildGradle("""
dependencies { implementation 'org.springframework.boot:spring-boot-starter:3.3.0' }
""", """
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' }
"""));
}

@Test
void testKotlinDependenciesMergeNewLines() {
String config = """
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
}
""";
K.CompilationUnit cu = parseKotlinConfig(config);
rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, true)), Assertions.buildGradleKts("""
dependencies { implementation("org.springframework.boot:spring-boot-starter:3.3.0") }
""", """
dependencies {
implementation("org.springframework.boot:spring-boot-starter:3.3.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") }"""));
}

@Test
void testKotlinDependenciesMerge() {
String config = """
dependencies {
implementation("org.springframework.boot:spring-boot-starter:3.5.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
}
""";
K.CompilationUnit cu = parseKotlinConfig(config);
rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, true)), Assertions.buildGradleKts("""
dependencies {
implementation("org.springframework.boot:spring-boot-starter:3.3.0")
runtimeOnly("org.postgresql:postgresql:42.7.0")
}
""", """
dependencies {
implementation("org.springframework.boot:spring-boot-starter:3.5.0")
runtimeOnly("org.postgresql:postgresql:42.7.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
}
"""));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,23 @@ void testKotlinAppendPlugin() {
"""));
}

@Test
void testKotlinAppendDefaultPlugin() {
rewriteRun(spec -> spec.recipe(new AddGradlePluginRecipe("checkstyle", null, true)),
Assertions.buildGradleKts("""
plugins {
kotlin("jvm") version "1.9.22"
}
group = "com.example"
version = "1.0"
""", """
plugins {
kotlin("jvm") version "1.9.22"
id("checkstyle")
}
group = "com.example"
version = "1.0"
"""));
}

}
Loading