Skip to content

Commit

Permalink
[env-manager] experimental env detection
Browse files Browse the repository at this point in the history
  • Loading branch information
rmannibucau committed Feb 12, 2024
1 parent ac16985 commit 7e9be16
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 18 deletions.
46 changes: 28 additions & 18 deletions env-manager/src/main/java/io/yupiik/dev/command/Env.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ public void run() {
throw new IllegalStateException(e);
}
}
if (conf.enableAutoDetection() && (tools == null || (tools.local().isEmpty()) && inlineProps.isEmpty())) {
logger.finest(() -> "Try auto-detecting environment");
inlineProps.putAll(rc.autoDetectProperties(tools == null ? null : tools.global()));
} else {
logger.finest(() -> "No auto-detection");
}

if ((tools == null || (tools.global().isEmpty() && tools.local().isEmpty())) && inlineProps.isEmpty()) { // nothing to do
return;
Expand All @@ -111,7 +117,27 @@ public void run() {
final var logger = Logger.getLogger("io.yupiik.dev");
final var useParentHandlers = logger.getUseParentHandlers();
final var messages = new ArrayList<String>();
final var tempHandler = new Handler() { // forward all standard messages to stderr and at debug level to avoid to break default behavior
final var tempHandler = captureLogHandler(logger, messages, useParentHandlers);
logger.setUseParentHandlers(false);
logger.addHandler(tempHandler);
try {
(tools == null ? rc.match(inlineProps) : rc.match(inlineProps, tools.local(), tools.global()))
.thenAccept(resolved -> createScript(resolved, export, quote, pathName, hasTerm, pathVar, tools, messages, comment))
.toCompletableFuture()
.get();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} catch (final ExecutionException e) {
throw new IllegalStateException(e.getCause());
} finally {
logger.setUseParentHandlers(useParentHandlers);
logger.removeHandler(tempHandler);
}
}

private Handler captureLogHandler(final Logger logger, final ArrayList<String> messages, final boolean useParentHandlers) {
return new Handler() { // forward all standard messages to stderr and at debug level to avoid to break default behavior
@Override
public void publish(final LogRecord record) {
// capture to forward messages in the shell when init is done (thanks eval call)
Expand All @@ -136,23 +162,6 @@ public void close() throws SecurityException {
flush();
}
};
logger.setUseParentHandlers(false);
logger.addHandler(tempHandler);

try {
(tools == null ? rc.match(inlineProps) : rc.match(inlineProps, tools.local(), tools.global()))
.thenAccept(resolved -> createScript(resolved, export, quote, pathName, hasTerm, pathVar, tools, messages, comment))
.toCompletableFuture()
.get();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} catch (final ExecutionException e) {
throw new IllegalStateException(e.getCause());
} finally {
logger.setUseParentHandlers(useParentHandlers);
logger.removeHandler(tempHandler);
}
}

private void createScript(final List<RcService.MatchedPath> rawResolved,
Expand Down Expand Up @@ -237,6 +246,7 @@ private void resetOriginalPath(final String export, final String pathName, final

@RootConfiguration("env")
public record Conf(
@Property(documentation = "EXPERIMENTAL. If enabled and no `.yemrc` nor `.sdkmanrc` setup any tool (ie empty does not count) then try to detect some well known tools like Java (there is a `pom.xml` and the compiler version can be extracted for example) and Maven (`pom.xml` presence). Note that it is done using `./` folder and bubbles up with a max of 10 levels.", defaultValue = "false") boolean enableAutoDetection,
@Property(documentation = "By default if `YEM_ORIGINAL_PATH` exists in the environment variables it is used as `PATH` base to not keep appending path to the `PATH` indefinively. This can be disabled setting this property to `false`", defaultValue = "false") boolean skipReset,
@Property(documentation = "Should `~/.yupiik/yem/rc` be ignored or not. If present it defines default versions and uses the same syntax than `yemrc`.", defaultValue = "System.getProperty(\"user.home\") + \"/.yupiik/yem/rc\"") String defaultRc,
@Property(documentation = "Enables to set inline a rc file, ex: `eval $(yem env --inlineRc 'java.version=17.0.9')`, you can use EOL too: `eval $(yem env --inlineRc 'java.version=17.\\njava.relaxed = true')`. Note that to persist the change even if you automatically switch from the global `yemrc` file the context, we set `YEM_$TOOLPATHVARNAME_OVERRIDEN` environment variable. To reset the value to the global configuration just `unset` this variable (ex: `unset YEM_JAVA_PATH_OVERRIDEN`). Note that you can also just set the values inline as args without that option: `eval $(yem env --java-version 17. --java-relaxed true ...)`.") String inlineRc,
Expand Down
178 changes: 178 additions & 0 deletions env-manager/src/main/java/io/yupiik/dev/shared/RcService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@
import io.yupiik.dev.provider.model.Version;
import io.yupiik.dev.shared.http.YemHttpClient;
import io.yupiik.fusion.framework.api.scope.ApplicationScoped;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
Expand All @@ -40,6 +48,7 @@
import static java.util.Locale.ROOT;
import static java.util.concurrent.CompletableFuture.allOf;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.logging.Level.FINEST;
import static java.util.stream.Collectors.toMap;

@ApplicationScoped
Expand Down Expand Up @@ -259,6 +268,101 @@ private Path auto(final Path from) {
.orElseGet(() -> from.resolve(".yemrc"));
}

public Map<String, String> autoDetectProperties(final Properties globalConf) {
final var props = new HashMap<String, String>();
autoDetectMvn(props, Path.of("."), globalConf, false, false, null);
return props;
}

// todo: refine before enabling automatically
private void autoDetectMvn(final Map<String, String> props, final Path folder, final Properties globalConf,
final boolean ignoreChildren, final boolean ignoreParent,
final SAXParserFactory providedFactory) {
if (props.containsKey("maven.version")) { // already setup
return;
}

try {
SAXParserFactory saxParserFactory = providedFactory;

final var pom = folder.resolve("pom.xml");
if (!Files.exists(pom)) {
return;
}

if (saxParserFactory == null) {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(false);
saxParserFactory.setValidating(false);
}

QuickMvnParser meta;
try (final var in = new BufferedInputStream(Files.newInputStream(pom))) {
meta = parsePom(saxParserFactory, in);
}

if (!ignoreParent && meta.parentRelativePath != null) {
autoDetectMvn(props, folder.resolve(meta.parentRelativePath), globalConf, true, false, saxParserFactory);
}

if (meta.requireMavenVersion != null) {
var version = meta.requireMavenVersion;
if (version.endsWith(",)") && version.startsWith("[")) { // only a minimum, no max
version = version.substring(1, version.length() - ",)".length());
props.put("maven.relaxed", "true");
if (version.startsWith("2.")) {
props.put("maven.version", "3."); // no joke, 2 is dead today
} else {
props.put("maven.version", version.length() > 2 ? version.substring(0, 2) : version);
}
} else if (!version.contains("(") && !version.contains(")") && !version.contains("[") && !version.contains("]")) {
props.put("maven.relaxed", "true");
props.put("maven.version", version.length() > 2 ? version.substring(0, 2) : version);
} else if (globalConf == null || !globalConf.containsKey("maven.version")) {
// todo: parse range? https://maven.apache.org/enforcer/enforcer-rules/versionRanges.html
props.put("maven.version", "3.");
props.put("maven.relaxed", "true");
}
}
if (meta.requireJavaVersion >= 8) {
final var existing = props.get("java.version");
props.put("java.version", Integer.toString(existing == null ?
meta.requireJavaVersion :
Math.max(meta.requireJavaVersion, Integer.parseInt(meta.requireMavenVersion))));
props.put("java.relaxed", "true");
}

if (!ignoreChildren && !meta.children.isEmpty()) {
for (final var child : meta.children) {
autoDetectMvn(props, folder.resolve(child), globalConf, false, true, saxParserFactory);
}
}

if (!ignoreChildren && !ignoreParent) { // root
if (globalConf == null || !globalConf.containsKey("maven.version")) {
props.putIfAbsent("maven.version", "3.");
props.putIfAbsent("maven.relaxed", "true");
}
if (globalConf == null || !globalConf.containsKey("java.version")) {
props.putIfAbsent("java.version", "");
props.putIfAbsent("java.relaxed", "true");
}
}
} catch (final IOException ioe) {
logger.log(FINEST, ioe, ioe::getMessage);
}
}

private QuickMvnParser parsePom(final SAXParserFactory factory, final InputStream stream) {
final var handler = new QuickMvnParser();
try {
factory.newSAXParser().parse(stream, handler);
} catch (final Exception e) {
// no-op: not parseable so ignoring
}
return handler;
}

public record ToolProperties(
int index,
String toolName,
Expand All @@ -277,4 +381,78 @@ public record MatchedPath(Path path, ToolProperties properties, Provider provide

public record Props(Properties global, Properties local) {
}

private static class QuickMvnParser extends DefaultHandler {
private final LinkedList<String> tags = new LinkedList<>();
private final Map<String, String> properties = new HashMap<>();
private String currentPlugin;
private StringBuilder text;

private int requireJavaVersion = 0;
private String requireMavenVersion;
private String parentRelativePath = "..";
private final List<String> children = new ArrayList<>();

@Override
public void startElement(final String uri, final String localName,
final String qName, final Attributes attributes) {
if ("relativePath".equals(qName) ||
"module".equals(qName) ||
("artifactId".equals(qName) && tags.size() >= 3) ||
("maven-compiler-plugin".equals(currentPlugin) && (
"release".equals(qName) || "target".equals(qName) || "source".equals(qName))) ||
(tags.size() == 2 && "properties".equals(tags.getLast())) ||
(tags.size() > 4 && "requireMavenVersion".equals(tags.getLast()) && "version".equals(qName))) {
text = new StringBuilder();
}
tags.add(qName);
}

@Override
public void characters(final char[] ch, final int start, final int length) {
if (text != null) {
text.append(new String(ch, start, length));
}
}

@Override
public void endElement(final String uri, final String localName, final String qName) {
tags.removeLast();
if ("relativePath".equals(qName) && "parent".equals(tags.getLast()) && tags.size() == 2) {
parentRelativePath = text.isEmpty() ? ".." : text.toString().strip();
} else if ("module".equals(qName) && "modules".equals(tags.getLast()) && tags.size() == 2 && !text.isEmpty()) {
children.add(text.toString().strip());
} else if ("artifactId".equals(qName) && "plugin".equals(tags.getLast()) && (tags.size() == 4 || tags.size() == 5 /* mgt */)) {
currentPlugin = text.toString().strip();
} else if ("plugin".equals(qName)) {
currentPlugin = null;
} else if (tags.size() == 2 && "properties".equals(tags.getLast())) {
properties.put(qName, text.toString().strip());
} else if ("requireMavenVersion".equals(tags.getLast()) && "version".equals(qName)) {
requireMavenVersion = text.toString().strip();
} else if ("maven-compiler-plugin".equals(currentPlugin) && (
"release".equals(qName) || "target".equals(qName) || "source".equals(qName))) {
var version = text.toString();
if (!version.isBlank()) {
int iterations = 10;
while (iterations-- > 0 && version.startsWith("${") && version.endsWith("}")) {
version = properties.get(version.substring("${".length(), version.length() - 1));
}
if (version != null) {
try {
final var end = version.indexOf('.');
requireJavaVersion = Math.max(
requireJavaVersion,
version.startsWith("1.8") ?
8 :
Integer.parseInt(version.substring(0, end < 0 ? version.length() : end)));
} catch (final NumberFormatException nfe) {
// no-op
}
}
}
}
text = null;
}
}
}

0 comments on commit 7e9be16

Please sign in to comment.