package org.graalvm.component.installer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import static org.graalvm.component.installer.BundleConstants.GRAALVM_CAPABILITY;
import org.graalvm.component.installer.jar.JarMetaLoader;
import org.graalvm.component.installer.model.ComponentInfo;
import org.graalvm.component.installer.persist.ComponentPackageLoader;
import org.graalvm.component.installer.remote.FileDownloader;
public final class GenerateCatalog {
private List<String> params = new ArrayList<>();
private List<String> locations = new ArrayList<>();
private String graalVersionPrefix;
private String graalVersionName;
private String forceVersion;
private String forceOS;
private String forceArch;
private String urlPrefix;
private final StringBuilder catalogContents = new StringBuilder();
private final StringBuilder = new StringBuilder();
private Environment env;
private String graalNameFormatString = "GraalVM %1s %2s/%3s";
private String graalVersionFormatString;
private static final Map<String, String> OPTIONS = new HashMap<>();
private static final String OPT_FORMAT_1 = "1";
private static final String OPT_FORMAT_2 = "2";
private static final String OPT_VERBOSE = "v";
private static final String OPT_GRAAL_PREFIX = "g";
private static final String OPT_GRAAL_NAME = "n";
private static final String OPT_GRAAL_NAME_FORMAT = "f";
private static final String OPT_URL_BASE = "b";
private static final String OPT_PATH_BASE = "p";
private static final String OPT_FORCE_VERSION = "e";
private static final String OPT_FORCE_OS = "o";
private static final String OPT_FORCE_ARCH = "a";
private static final String OPT_SEARCH_LOCATION = "l";
static {
OPTIONS.put(OPT_FORMAT_1, "");
OPTIONS.put(OPT_FORMAT_2, "");
OPTIONS.put(OPT_VERBOSE, "");
OPTIONS.put(OPT_GRAAL_PREFIX, "s");
OPTIONS.put(OPT_FORCE_VERSION, "s");
OPTIONS.put(OPT_FORCE_OS, "s");
OPTIONS.put(OPT_GRAAL_NAME_FORMAT, "s");
OPTIONS.put(OPT_GRAAL_NAME, "s");
OPTIONS.put(OPT_FORCE_ARCH, "s");
OPTIONS.put(OPT_GRAAL_NAME, "");
OPTIONS.put(OPT_URL_BASE, "s");
OPTIONS.put(OPT_PATH_BASE, "s");
OPTIONS.put(OPT_SEARCH_LOCATION, "s");
}
private Map<String, GraalVersion> graalVMReleases = new LinkedHashMap<>();
public static void main(String[] args) throws IOException {
new GenerateCatalog(args).run();
System.exit(0);
}
private GenerateCatalog(String[] args) {
this.params = new ArrayList<>(Arrays.asList(args));
}
private static byte[] computeHash(File f) throws IOException {
MessageDigest fileDigest;
try {
fileDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ex) {
throw new IOException("Cannot compute digest " + ex.getLocalizedMessage(), ex);
}
ByteBuffer bb = ByteBuffer.allocate(2048);
boolean updated = false;
try (
InputStream is = new FileInputStream(f);
ReadableByteChannel bch = Channels.newChannel(is)) {
int read;
while (true) {
read = bch.read(bb);
if (read < 0) {
break;
}
bb.flip();
fileDigest.update(bb);
bb.clear();
updated = true;
}
}
if (!updated) {
fileDigest.update(new byte[0]);
}
return fileDigest.digest();
}
static String digest2String(byte[] digest) {
StringBuilder sb = new StringBuilder(digest.length * 3);
for (int i = 0; i < digest.length; i++) {
sb.append(String.format("%02x", (digest[i] & 0xff)));
}
return sb.toString();
}
static class Spec {
File f;
String u;
String relativePath;
Spec(File f, String u) {
this.f = f;
this.u = u;
}
}
static class GraalVersion {
String version;
String os;
String arch;
GraalVersion(String version, String os, String arch) {
this.version = version;
this.os = os;
this.arch = arch;
}
}
private List<Spec> componentSpecs = new ArrayList<>();
private Path pathBase = null;
public void run() throws IOException {
readCommandLine();
downloadFiles();
generateCatalog();
generateReleases();
System.out.println(catalogHeader);
System.out.println(catalogContents);
}
private void readCommandLine() throws IOException {
SimpleGetopt getopt = new SimpleGetopt(OPTIONS) {
@Override
public RuntimeException err(String messageKey, Object... args) {
ComponentInstaller.printErr(messageKey, args);
System.exit(1);
return null;
}
}.ignoreUnknownCommands(true);
getopt.setParameters(new LinkedList<>(params));
getopt.process();
this.env = new Environment(null, getopt.getPositionalParameters(), getopt.getOptValues());
this.env.setAllOutputToErr(true);
String pb = env.optValue(OPT_PATH_BASE);
if (pb != null) {
pathBase = SystemUtils.fromUserString(pb).toAbsolutePath();
}
urlPrefix = env.optValue(OPT_URL_BASE);
graalVersionPrefix = env.optValue(OPT_GRAAL_PREFIX);
if (graalVersionPrefix != null) {
graalVersionName = env.optValue(OPT_GRAAL_NAME);
if (graalVersionName == null) {
throw new IOException("Graal prefix specified, but no human-readable name");
}
}
forceVersion = env.optValue(OPT_FORCE_VERSION);
forceOS = env.optValue(OPT_FORCE_OS);
forceArch = env.optValue(OPT_FORCE_ARCH);
if (env.hasOption(OPT_FORMAT_1)) {
formatVer = 1;
} else if (env.hasOption(OPT_FORMAT_2)) {
formatVer = 2;
}
String s = env.optValue(OPT_GRAAL_NAME_FORMAT);
if (s != null) {
graalNameFormatString = s;
}
switch (formatVer) {
case 1:
graalVersionFormatString = "%s_%s_%s";
break;
case 2:
graalVersionFormatString = "%2$s_%3$s/%1$s";
break;
default:
throw new IllegalStateException();
}
if (env.hasOption(OPT_SEARCH_LOCATION)) {
Path listFrom = Paths.get(env.optValue("l"));
Files.walk(listFrom).filter((p) -> p.toString().endsWith(".jar")).forEach(
(p) -> locations.add(p.toString()));
} else {
while (env.hasParameter()) {
locations.add(env.nextParameter());
}
}
for (String spec : locations) {
File f = null;
String u = null;
int eq = spec.indexOf('=');
if (eq != -1) {
f = new File(spec.substring(0, eq));
if (!f.exists()) {
throw new FileNotFoundException(f.toString());
}
String uriPart = spec.substring(eq + 1);
u = uriPart;
} else {
f = new File(spec);
if (!f.exists()) {
f = null;
u = spec;
}
}
addComponentSpec(f, u);
}
}
private void addComponentSpec(File f, String u) {
Spec spc = new Spec(f, u);
if (f != null) {
if (pathBase != null) {
spc.relativePath = pathBase.relativize(f.toPath().toAbsolutePath()).toString();
}
}
componentSpecs.add(spc);
}
private URL createURL(String spec) throws MalformedURLException {
if (urlPrefix != null) {
return new URL(new URL(urlPrefix), spec);
} else {
return new URL(spec);
}
}
private void downloadFiles() throws IOException {
for (Spec spec : componentSpecs) {
if (spec.f == null) {
FileDownloader dn = new FileDownloader(spec.u, createURL(spec.u), env);
dn.setDisplayProgress(true);
dn.download();
spec.f = dn.getLocalFile();
}
}
}
private String os;
private String arch;
private String version;
private int formatVer = 1;
private String findComponentPrefix(ComponentInfo info) {
Map<String, String> m = info.getRequiredGraalValues();
if (graalVersionPrefix != null) {
arch = os = null;
version = graalVersionPrefix;
return graalVersionPrefix;
}
if (forceVersion != null) {
version = forceVersion;
} else {
switch (formatVer) {
case 1:
version = info.getVersionString();
break;
case 2:
version = info.getVersion().toString();
break;
}
}
return String.format(graalVersionFormatString,
version,
os = forceOS != null ? forceOS : m.get(CommonConstants.CAP_OS_NAME),
arch = forceArch != null ? forceArch : m.get(CommonConstants.CAP_OS_ARCH));
}
private void generateReleases() {
for (String prefix : graalVMReleases.keySet()) {
GraalVersion ver = graalVMReleases.get(prefix);
String vprefix;
String n;
if (ver.os == null) {
vprefix = graalVersionPrefix;
n = graalVersionName;
} else {
vprefix = String.format(graalVersionFormatString, ver.version, ver.os, ver.arch, "");
n = String.format(graalNameFormatString, ver.version, ver.os, ver.arch, "");
}
catalogHeader.append(GRAALVM_CAPABILITY).append('.').append(vprefix).append('=').append(n).append('\n');
if (ver.os == null) {
break;
}
}
}
private void generateCatalog() throws IOException {
for (Spec spec : componentSpecs) {
File f = spec.f;
byte[] hash = computeHash(f);
String hashString = digest2String(hash);
try (JarFile jf = new JarFile(f)) {
ComponentPackageLoader ldr = new JarMetaLoader(jf, hashString, env);
ComponentInfo info = ldr.createComponentInfo();
String prefix = findComponentPrefix(info);
if (!graalVMReleases.containsKey(prefix)) {
graalVMReleases.put(prefix, new GraalVersion(version, os, arch));
}
Manifest mf = jf.getManifest();
if (mf == null) {
throw new IOException("No manifest in " + spec);
}
String tagString;
if (formatVer < 2 || info.getTag() == null || info.getTag().isEmpty()) {
tagString = "";
} else {
tagString = "/" + hashString;
}
Attributes atts = mf.getMainAttributes();
String bid = atts.getValue(BundleConstants.BUNDLE_ID).toLowerCase().replace("-", "_");
String bl = atts.getValue(BundleConstants.BUNDLE_NAME);
if (bid == null) {
throw new IOException("Missing bundle id in " + spec);
}
if (bl == null) {
throw new IOException("Missing bundle name in " + spec);
}
String name;
prefix += tagString;
if (spec.u != null) {
name = spec.u.toString();
} else {
name = spec.relativePath != null ? spec.relativePath : f.getName();
}
int pos;
while ((pos = name.indexOf("${")) != -1) {
int endPos = name.indexOf("}", pos + 1);
if (endPos == -1) {
break;
}
String key = name.substring(pos + 2, endPos);
String repl = info.getRequiredGraalValues().get(key);
if (repl == null) {
switch (key) {
case "version":
repl = version;
break;
case "os":
repl = os;
break;
case "arch":
repl = arch;
break;
case "comp_version":
repl = info.getVersionString();
break;
default:
throw new IllegalArgumentException(key);
}
}
if (repl == null) {
throw new IllegalArgumentException(key);
}
String toReplace = "${" + key + "}";
name = name.replace(toReplace, repl);
}
String url = (urlPrefix == null || urlPrefix.isEmpty()) ? name : urlPrefix + "/" + name;
String sel;
String hashSuffix;
switch (formatVer) {
case 1:
sel = "Component.{0}.{1}";
hashSuffix = "-hash";
break;
case 2:
sel = "Component.{0}/{1}";
hashSuffix = "-hash";
break;
default:
throw new IllegalStateException();
}
catalogContents.append(MessageFormat.format(
sel + "={2}\n", prefix, bid, url));
catalogContents.append(MessageFormat.format(
sel + hashSuffix + "={2}\n", prefix, bid, hashString));
for (Object a : atts.keySet()) {
String key = a.toString();
String val = atts.getValue(key);
if (key.startsWith("x-GraalVM-Message-")) {
continue;
}
catalogContents.append(MessageFormat.format(
sel + "-{2}={3}\n", prefix, bid, key, val));
}
}
}
}
}