Skip to content
Open
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
6 changes: 5 additions & 1 deletion ant/project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jlink.java.gc.version="gc-ver-is-empty"
# Bundle a locally built copy of Java instead
# jlink.java.target=/path/to/custom/jdk-x.x.x

#jlink.api.url=https://api.bell-sw.com/v1/liberica/releases
#jlink.api.token=
#jlink.api.newest=true

# Skip bundling the java runtime
# jre.skip=true

Expand All @@ -60,4 +64,4 @@ provision.dir=${dist.dir}/provision
java.mask.tray=true

# Workaround to delay expansion of $${foo} (e.g. shell scripts)
dollar=$
dollar=$
99 changes: 71 additions & 28 deletions src/qz/build/Fetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All @@ -23,9 +29,10 @@ public class Fetcher {
public enum Format {
ZIP(".zip"),
TARBALL(".tar.gz"),
JSON(".json"),
UNKNOWN(null);

String suffix;
final String suffix;
Format(String suffix) {
this.suffix = suffix;
}
Expand All @@ -35,8 +42,12 @@ public String getSuffix() {
}

public static Format parse(String url) {
if(url.contains("?")) {
url = url.substring(0, url.lastIndexOf("?"));
log.debug("Stripped parameters from URL to help detecting file type: '{}'", url);
}
for(Format format : Format.values()) {
if (url.endsWith(format.getSuffix())) {
if (format.getSuffix() != null && url.endsWith(format.getSuffix())) {
return format;
}
}
Expand All @@ -50,55 +61,87 @@ public static void main(String ... args) throws IOException {
new Fetcher("jlink/qz-tray-src_x.x.x", "https://github.com/qzind/tray/archive/master.tar.gz").fetch().uncompress();
}

String resourceName;
String url;
Format format;
Path rootDir;
File tempArchive;
final String resourceName;
final String url;
final Format format;
final Path rootDir;
final Map<String, String> headers;

File tempFile;
File tempExtracted;
File extracted;

public Fetcher(String resourceName, String url) {
this.url = url;
this.resourceName = resourceName;
this.format = Format.parse(url);
// Try to calculate out/
this.rootDir = SystemUtilities.getJarParentPath().getParent();
}

@SuppressWarnings("unused")
public Fetcher(String resourceName, String url, Format format, String rootDir) {
public Fetcher(String resourceName, String url, Format format, Path rootDir, Map<String, String> headers) {
this.resourceName = resourceName;
this.url = url;
this.format = format;
this.rootDir = Paths.get(rootDir);
this.rootDir = rootDir;
this.headers = headers;
}

public Fetcher(String resourceName, String url, Format format, Map<String, String> headers) {
this(resourceName, url, format, SystemUtilities.getJarParentPath().getParent(), headers);
}

public Fetcher(String resourceName, String url, Map<String, String> headers) {
this(resourceName, url, Format.parse(url), headers);
}

public Fetcher(String resourceName, String url) {
this(resourceName, url, null);
}

public Fetcher fetch() throws IOException {
extracted = new File(rootDir.toString(), resourceName);
if(extracted.isDirectory() && extracted.exists()) {
if(extracted.isDirectory() && extracted.exists() && Objects.requireNonNull(extracted.listFiles()).length > 0) {
log.info("Resource '{}' from [{}] has already been downloaded and extracted. Using: [{}]", resourceName, url, extracted);
} else {
tempExtracted = new File(rootDir.toString(), resourceName + "~tmp");
if(tempExtracted.exists()) {
FileUtils.deleteDirectory(tempExtracted);
}
// temp directory to thwart partial extraction
tempExtracted.mkdirs();
tempArchive = File.createTempFile(resourceName, ".zip");
log.info("Fetching '{}' from [{}] and saving to [{}]", resourceName, url, tempArchive);
FileUtils.copyURLToFile(new URL(url), tempArchive);
if(tempExtracted.mkdirs()) {
tempFile = File.createTempFile(resourceName, format == Format.JSON ? ".json" : ".zip");
log.info("Fetching '{}' from [{}] and saving to [{}]", resourceName, url, tempFile);
copyUrlToFile(new URL(url), tempFile.toPath(), headers);
} else {
throw new IOException(String.format("Unable to create directory for jdk extraction '%s'", tempExtracted));
}
}
return this;
}

public void copyUrlToFile(URL url, Path targetPath, Map<String, String> headers) throws IOException {
HttpRequest.Builder requestBuilder;
try {
requestBuilder = HttpRequest.newBuilder().uri(url.toURI()).GET();
if(headers != null) {
headers.forEach(requestBuilder::header);
}
HttpRequest request = requestBuilder.build();

// stream response to file
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofFile(
targetPath,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
));
} catch(URISyntaxException e) {
throw new IOException(String.format("Invalid URI specified '%s'", url), e);
} catch(InterruptedException e) {
throw new IOException(String.format("Request interrupted '%s'", url), e);
}
}

public String uncompress() throws IOException {
if(tempArchive != null) {
log.info("Unzipping '{}' from [{}] to [{}]", resourceName, tempArchive, tempExtracted);
if(tempFile != null) {
log.info("Unzipping '{}' from [{}] to [{}]", resourceName, tempFile, tempExtracted);
if(format == Format.ZIP) {
unzip(tempArchive.getAbsolutePath(), tempExtracted);
unzip(tempFile.getAbsolutePath(), tempExtracted);
} else {
untar(tempArchive.getAbsolutePath(), tempExtracted);
untar(tempFile.getAbsolutePath(), tempExtracted);
}
log.info("Moving [{}] to [{}]", tempExtracted, extracted);
tempExtracted.renameTo(extracted);
Expand Down
117 changes: 116 additions & 1 deletion src/qz/build/JLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
package qz.build;

import com.github.zafarkhaja.semver.Version;
import javafx.util.Pair;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.jlink.Platform;
import qz.build.jlink.Vendor;
import qz.build.jlink.Url;
Expand All @@ -25,6 +29,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.*;
import java.util.*;

Expand Down Expand Up @@ -141,14 +146,124 @@ private static boolean needsDownload(Version want, Version installed) {
return downloadJdk;
}

private HashMap<String,String> getApiHeaders() {
HashMap<String,String> headers = new HashMap<>();

String apiKey = System.getProperty("jlink.api.token");
if(apiKey == null) {
return headers;
}

switch(javaVendor) {
case BELLSOFT:
headers.put("Authorization", String.format("Bearer %s", apiKey));
break;
case ECLIPSE:
default:
throw new UnsupportedOperationException(String.format("API headers are not yet implemented for '%s'", javaVendor));
}
return headers;
}

/**
* Grabs a <code>URL,Version</code> pair from an API call
* TODO: Make this more vendor-agnostic
*/
private Pair<URL,Version> getApiUrl(Arch arch, Platform platform, HashMap<String,String> headers) throws IOException {
String apiUrl = System.getProperty("jlink.api.url");
String wantsNewest = System.getProperty("jlink.api.newest"); // defaults to true

Version bestVersion = Version.of(0);
URL bestUrl = null;

if(apiUrl != null) {
// assume we always want the newest build
boolean newest = wantsNewest == null || wantsNewest.isBlank() || Boolean.parseBoolean(wantsNewest);
// e.g. product-info-bell-soft-12345.json
String hostString = new URL(apiUrl).getHost().replaceAll("\\.", "-");
long hoursSinceEpoch = System.currentTimeMillis() / 3600000;
String resourceName = String.format("product-info-%s-%s.json", hostString, hoursSinceEpoch);

// add filters to narrow our results
apiUrl += "?" + javaVendor.getApiPlatform(platform) +
"&" + javaVendor.getApiArch(arch) +
"&" + javaVendor.getApiMajorVersion(javaSemver);

File productInfo = new Fetcher(resourceName, apiUrl, Fetcher.Format.JSON, headers)
.fetch().tempFile;

try {
JSONArray jsonArray = new JSONArray(FileUtilities.readLocalFile(productInfo.toPath()));

for(int i = 0; i < jsonArray.length(); i++) {
if(jsonArray.get(i) instanceof JSONObject) {
JSONObject entry = jsonArray.getJSONObject(i);
if(!entry.optString("packageType", "").equals("zip") ||
!entry.optString("bundleType", "").equals("jdk")) {
continue;
}

String versionFound = entry.optString("version");
String downloadUrl = entry.optString("downloadUrl");
if(newest) {
if (versionFound != null) {
Version foundVersion = SystemUtilities.getJavaVersion(versionFound);
if (foundVersion.isHigherThan(bestVersion)) {
bestVersion = foundVersion;
bestUrl = new URL(downloadUrl);
} else if (foundVersion.equals(bestVersion)) {
// handle '+nnn'
if (foundVersion.buildMetadata().isPresent() && bestVersion.buildMetadata().isPresent()) {
if (foundVersion.buildMetadata().get().compareTo(bestVersion.buildMetadata().get()) > 0) {
bestVersion = foundVersion;
bestUrl = new URL(downloadUrl);
}
}
}
}
} else {
if(javaVersion.equals(versionFound)) {
bestVersion = SystemUtilities.getJavaVersion(versionFound);
bestUrl = new URL(downloadUrl);
}
}
} else {
log.warn("Entry {} is not a JSONObject", i);
}
}
}
catch(JSONException e) {
throw new IOException("Error parsing download file", e);
}
}
return new Pair<>(bestUrl, bestVersion);
}

/**
* Download the JDK and return the path it was extracted to
*/
private String downloadJdk(Arch arch, Platform platform) throws IOException {
String url = new Url(this.javaVendor).format(arch, platform, this.gcEngine, this.javaSemver, this.javaVersion, this.gcVersion);

// Check to see if we should use an API download
HashMap<String, String> apiHeaders = getApiHeaders();
Pair<URL,Version> apiPair = getApiUrl(arch, platform, apiHeaders);
if(apiPair.getKey() != null) {
log.info("Using Java '{} from '{}' since an API url was provided", apiPair.getValue(), apiPair.getKey());
url = apiPair.getKey().toString();
javaSemver = apiPair.getValue();
}

// Saves to out e.g. "out/jlink/jdk-AdoptOpenjdk-amd64-platform-11_0_7"
String extractedJdk = new Fetcher(String.format("jlink/jdk-%s-%s-%s-%s", javaVendor.value(), arch, platform.value(), javaSemver.toString().replaceAll("\\+", "_")), url)
String resourceName = String.format("jlink/jdk%s-%s-%s-%s-%s",
apiPair.getKey() == null ? "" : "-api",
javaVendor.value(),
arch,
platform.value(),
javaSemver.toString().replaceAll("\\+", "_"));


String extractedJdk = new Fetcher(resourceName, url)
.fetch()
.uncompress();

Expand Down
44 changes: 44 additions & 0 deletions src/qz/build/jlink/Vendor.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,50 @@ public String getUrlArch(Arch arch) {
}
}

public String getApiPlatform(Platform platform) {
switch(this) {
case BELLSOFT:
// Assume they're the same unless we know otherwise
return String.format("os=%s",getUrlPlatform(platform));
case ECLIPSE:
default:
throw new UnsupportedOperationException(String.format("Filtering API by os is not yet supported for this vendor (%s)", this));
}
}

public String getApiMajorVersion(Version version) {
switch(this) {
case BELLSOFT:
// Assume they're the same unless we know otherwise
return String.format("version-feature=%d", version.majorVersion());
case ECLIPSE:
default:
throw new UnsupportedOperationException(String.format("Filtering API by major version is not yet supported for this vendor (%s)", this));
}
}

public String getApiArch(Arch arch) {
switch(this) {
case BELLSOFT:
switch(arch) {
case ARM32:
case AARCH64:
return String.format("arch=arm&bitness=%s", arch.getBitness());
case X86:
case X86_64:
return String.format("arch=x86&bitness=%s", arch.getBitness());
case RISCV32:
case RISCV64:
return String.format("arch=riscv&bitness=%s", arch.getBitness());
case PPC64:
return String.format("arch=ppc&bitness=%s", arch.getBitness());
}
case ECLIPSE:
default:
throw new UnsupportedOperationException(String.format("Filtering API by arch '%s' is not yet supported for this vendor (%s)", arch, this));
}
}

/**
* Map Vendor to Platform name
*/
Expand Down
7 changes: 7 additions & 0 deletions src/qz/build/provision/params/Arch.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ public enum Arch {
UNKNOWN();

private HashSet<String> aliases = new HashSet<>();
private int bitness;

Arch(String ... aliases) {
this.aliases.add(name().toLowerCase(Locale.ENGLISH));
this.aliases.addAll(Arrays.asList(aliases));
this.bitness = name().endsWith("64") ? 64 : 32;
}

public static Arch parseStrict(String input) throws UnsupportedOperationException {
Expand Down Expand Up @@ -63,6 +66,10 @@ public static String serialize(HashSet<Arch> archList) {
return StringUtils.join(archList, "|");
}

public int getBitness() {
return bitness;
}

@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
Expand Down
Loading