Skip to content

Instantly share code, notes, and snippets.

@fintanmm
Last active November 13, 2023 09:37
Show Gist options
  • Save fintanmm/0fec175b2d8ae4a04f3052ecfcb6def0 to your computer and use it in GitHub Desktop.
Save fintanmm/0fec175b2d8ae4a04f3052ecfcb6def0 to your computer and use it in GitHub Desktop.
Jbang script to clean up sdk's installed by sdkman.
///usr/bin/env jbang "$0" "$@" ; exit $?
/**
* <h1>sdk-clean</h1>
*/
//DEPS info.picocli:picocli:4.5.0
//DEPS info.picocli:picocli-codegen:4.5.0
//DEPS ch.qos.reload4j:reload4j:1.2.19
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@Command(name = "sdk-clean", description = "Cleans up unused SDKs from the SDKMAN installation", mixinStandardHelpOptions = true, version = "sdk-clean 1.0")
public class SdkClean implements Runnable {
private static final Logger logger = Logger.getLogger(SdkClean.class.getName());
private static final String SDKMANCOMMAND = "source %s/bin/sdkman-init.sh && sdk %s";
private static String sdkmanHome;
@CommandLine.Option(names = {"-n", "--keep-last-n-sdks"}, description = "Keep the last n SDKs (default: 3)")
private static int keepLastNSdks = 3;
@CommandLine.Option(names = {"-sdk"}, description = "The sdk to be removed.", required = true)
private static String installedSdk = "quarkus";
public static void main(String[] args) {
BasicConfigurator.configure();
// Get the sdkman installation directory
sdkmanHome = Optional.ofNullable(System.getenv("SDKMAN_DIR")).orElse("/usr/local/sdkman");
if (isSdkmanInstalled()) {
new CommandLine(new SdkClean()).execute(args);
} else {
logger.debug("sdkman is not installed");
System.exit(0);
}
}
/**
* Tests if sdkman is installed.
* @return boolean True if sdkman is installed, false otherwise.
*/
public static boolean isSdkmanInstalled() {
try {
Process process = new ProcessBuilder("bash", "-c", SDKMANCOMMAND.formatted(sdkmanHome, "version")).start();
int exitCode = process.waitFor();
return exitCode == process.waitFor();
} catch (IOException | InterruptedException e) {
logger.debug("sdk command not found", e);
return false;
}
}
@Override
public void run() {
try {
List<String> listOfInstalledSdks = parseInstalledSdk();
listOfInstalledSdks.stream().sorted().forEach(version -> {
if (!isInUse()) {
try {
// if the SDK is not in the list of the last three SDKs, uninstall it
if (!keepLastNSdks(listOfInstalledSdks).contains(version)) {
uninstall(version);
}
} catch (IOException | InterruptedException e) {
logger.debug("Error uninstalling SDK", e);
Thread.currentThread().interrupt();
}
}
});
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
/**
* Checks if an SDK is in use by any Java processes.
* @return True if the SDK is in use, false otherwise.
*/
private static boolean isInUse() {
// Check if the SDK is in use by any Java processes
boolean isInUse = false;
Stream<ProcessHandle> liveProcesses = ProcessHandle.allProcesses();
Optional<ProcessHandle> process = liveProcesses.filter(p -> null != p.info())
.filter(p -> p.info().command().isPresent())
.filter(p -> p.info().command().get().contains(installedSdk))
.findFirst();
if (process.isPresent()) {
isInUse = true;
}
return isInUse;
}
/**
* Keeps the last three SDKs.
*
* @param installedSdks The list of installed SDKs.
* @return List<String> The last three SDKs.
*/
public static List<String> keepLastNSdks(List<String> installedSdks) {
return installedSdks.stream()
.sorted()
.skip(Math.max(0, installedSdks.size() - Optional.of(keepLastNSdks).orElse(3)))
.toList();
}
/**
* Uninstalls an SDK.
* @param version The version to uninstall.
*/
private static void uninstall(String version) throws IOException, InterruptedException {
if (!version.isEmpty() && !version.contains("-")){
logger.debug("Uninstalling %s %s".formatted(installedSdk, version));
logger.debug(SDKMANCOMMAND.formatted(sdkmanHome, "uninstall %s %s".formatted(installedSdk, version)));
new ProcessBuilder("bash", "-c", SDKMANCOMMAND.formatted(sdkmanHome, "uninstall %s %s".formatted(installedSdk, version))).start().waitFor();
}
}
/**
* Parses the output of the sdk list command.
*
* @return List<String> The list of installed SDKs.
* @throws IOException If an I/O error occurs.
* @throws InterruptedException If the process is interrupted.
*/
private List<String> parseInstalledSdk() throws IOException, InterruptedException {
File sdkList = createTempFile();
List<String> versions = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(sdkList.toPath())) {
boolean startParsing = false;
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("Available")) {
startParsing = true;
continue;
}
if (startParsing && !line.startsWith("=")) {
versions.addAll(extract(line));
}
}
}
Files.delete(sdkList.toPath());
return versions;
}
/**
* Creates a temporary file containing the output of the sdk list command.
* @return File The temporary file.
*/
private File createTempFile() throws IOException, InterruptedException {
File sdkList = File.createTempFile("sdk-list", ".txt");
String command = SDKMANCOMMAND.formatted(sdkmanHome, " list %s > %s".formatted(installedSdk, sdkList.getAbsolutePath()));
new ProcessBuilder("bash", "-c", command).start().waitFor();
return sdkList;
}
/**
* Extracts the versions from the line.
* @param version The line.
* @return List<String> The list of versions.
*/
private List<String> extract(String version) {
Pattern pattern = Pattern.compile("\\*\\s*(\\S+)");
Matcher matcher = pattern.matcher(version);
if (matcher.find()) {
return extract(matcher);
} else {
return extractTokens(version);
}
}
/**
* Extracts the matches from the matcher.
* @param matcher The matcher.
* @return List<String> The list of matches.
*/
private List<String> extract(Matcher matcher) {
return Stream.generate(matcher::group)
.limit(matcher.groupCount())
.map(match -> match.replaceFirst("\\*", "").trim())
.toList();
}
/**
* Extracts the tokens from the tokens.
* @param tokens The tokens.
* @return List<String> The list of tokens.
*/
private List<String> extractTokens(String tokens) {
String[] t = tokens.split("\\s+");
return Stream.of(t)
.filter(token -> token.startsWith(">") || token.startsWith("*"))
.map(token -> token.replaceFirst("[>*]", "").trim())
.toList();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment