-
Notifications
You must be signed in to change notification settings - Fork 11
/
Download.java
453 lines (399 loc) · 16 KB
/
Download.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
/*
* Copyright (c) 2022, Oracle and/or its affiliates.
*
* This source code is licensed under the UPL license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.WRITE;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.net.URI;
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.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.regex.Pattern;
/** Download a JDK build. */
public class Download {
/** Main entry-point. */
public static void main(String... args) {
main(Boolean.getBoolean(/*-D*/ "ry-run"), args);
}
/** Entry-point also used by tests. */
static void main(boolean dryRun, String... args) {
// Pre-allocate action outputs
var outputs = new TreeMap<String, String>();
outputs.put("archive", "NOT-SET");
outputs.put("version", "NOT-SET");
try {
if (args.length == 0) {
throw new Error("Usage: Download URI or WEBSITE RELEASE VERSION");
}
var deque = new ArrayDeque<>(List.of(args));
var first = deque.removeFirst(); // URI or WEBSITE
// Determine website from first argument
var website =
Website.find(first).orElseThrow(() -> new Error("Could not find website for " + first));
GitHub.debug("website: " + website);
// Create JDK descriptor
var jdk =
new JDK(
deque.isEmpty() ? "ga" : deque.removeFirst().toLowerCase(),
deque.isEmpty() ? "latest" : deque.removeFirst().toLowerCase(),
deque.isEmpty() ? JDK.computeOsName() : deque.removeFirst(),
deque.isEmpty() ? JDK.computeOsArch() : deque.removeFirst(),
deque.isEmpty() ? JDK.computeFileType() : deque.removeFirst());
GitHub.debug("jdk: " + jdk);
// Select or find URI based on the JDK descriptor
var uri =
args.length == 1
? first
: website.findUri(jdk).orElseThrow(() -> new Error("Could not find URI of " + jdk));
GitHub.debug("uri: " + uri);
if (!(uri.endsWith(".tar.gz") || uri.endsWith(".zip"))) {
throw new IllegalArgumentException("URI must end with `.tar.gz` or `.zip`: " + uri);
}
// Emit warning when using an archived JDK build
if (website.isArchivedUri(uri)) {
GitHub.warn(
"""
JDK resolved to an archived build!
These older versions of the JDK are provided to help developers debug issues in older systems.
They are not updated with the latest security patches and are not recommended for use in production.
""");
}
// Acquire JDK archive
var archive = website.computeArchivePath(uri);
GitHub.debug("archive: " + archive);
var downloader = new Downloader(archive, uri);
if (website.isMovingResourceUri(uri)) {
downloader.checkSizeAndDeleteIfDifferent();
}
downloader.downloadArchive(dryRun);
downloader.verifyChecksums(website.getChecksum(uri));
System.out.printf("Archive %s in %s%n", archive.getFileName(), archive.getParent().toUri());
// Set outputs
outputs.put("archive", archive.toString());
var digit = Character.isDigit(jdk.release.charAt(0));
outputs.put("version", website.computeVersionString(uri, digit ? "PARSE_URI" : "HASH_URI"));
} catch (Exception exception) {
exception.printStackTrace(System.err);
GitHub.error("Error detected: " + exception);
} finally {
if (dryRun) {
System.out.println("Dry-run of run with " + List.of(args));
for (var output : outputs.entrySet()) {
System.out.println(" - " + output.getKey() + '=' + output.getValue());
}
} else {
outputs.forEach(GitHub::setOutput);
}
}
}
record JDK(String release, String version, String os, String arch, String type) {
static String computeOsName() {
var name = System.getProperty("os.name").toLowerCase();
if (name.contains("win")) return "windows";
if (name.contains("mac")) return "macos";
return "linux";
}
static String computeOsArch() {
var arch = System.getProperty("os.arch", "x64");
if (arch.equals("amd64")) return "x64";
if (arch.equals("x86_64")) return "x64";
return arch;
}
static String computeFileType() {
var name = System.getProperty("os.name").toLowerCase();
return name.contains("win") ? "zip" : "tar.gz";
}
}
/** Download helper. */
static class Downloader {
final Path archive;
final String uri;
final Browser browser;
Downloader(Path archive, String uri) {
this.archive = archive;
this.uri = uri;
this.browser = new Browser();
}
void checkSizeAndDeleteIfDifferent() throws Exception {
if (Files.notExists(archive)) return;
var cachedSize = Files.size(archive);
GitHub.debug("Cached size: " + cachedSize);
var remoteSize = browser.head(uri).headers().firstValueAsLong("content-length").orElse(-1);
GitHub.debug("Remote size: " + remoteSize);
if (cachedSize == remoteSize) return;
Files.delete(archive);
}
void downloadArchive(boolean dryRun) throws Exception {
if (Files.exists(archive)) return;
var head = browser.head(uri);
GitHub.debug(head.toString());
if (dryRun) {
return;
}
var response = browser.download(uri, archive);
GitHub.debug(response.toString());
}
void verifyChecksums(String checksum) throws Exception {
if (Files.notExists(archive)) return;
var cached = computeChecksum(archive);
GitHub.debug("Cached checksum: " + cached);
var remoteChecksum = findRemoteChecksum(checksum);
if (remoteChecksum.isEmpty()) {
GitHub.warn("Checksum not available for: " + uri);
return;
}
var remote = remoteChecksum.get();
GitHub.debug("Remote checksum: " + remote);
if (cached.equals(remote)) return;
var message = "Checksum verification failed, deleting cached archive";
Files.delete(archive);
GitHub.error(message);
throw new AssertionError(message);
}
String computeChecksum(Path path) {
try {
var md = MessageDigest.getInstance("SHA-256");
try (var input = new BufferedInputStream(new FileInputStream(path.toFile()));
var output = new DigestOutputStream(OutputStream.nullOutputStream(), md)) {
input.transferTo(output);
}
var length = md.getDigestLength() * 2;
return String.format("%0" + length + "x", new BigInteger(1, md.digest()));
} catch (IOException exception) {
throw new UncheckedIOException(exception);
} catch (NoSuchAlgorithmException exception) {
var algorithms = Security.getAlgorithms("MessageDigest");
throw new IllegalArgumentException(exception + ": " + algorithms);
}
}
Optional<String> findRemoteChecksum(String checksum) throws Exception {
if (!checksum.startsWith("https://")) return Optional.of(checksum);
if (browser.head(checksum).statusCode() == 200) return Optional.of(browser.browse(checksum));
return Optional.empty();
}
}
/**
* GitHub Actions helper.
*
* @see <a
* href="https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions">Workflow
* commands for GitHub Actions</a>
*/
static class GitHub {
/** Sets an action's output parameter. */
static void setOutput(String name, Object value) {
if (name.isBlank() || value.toString().isBlank()) { // implicit null checks included
throw new IllegalArgumentException("name or value are blank: " + name + "=" + value);
}
var githubOutput = System.getenv("GITHUB_OUTPUT");
if (githubOutput == null) {
throw new AssertionError("No such environment variable: GITHUB_OUTPUT");
}
try {
var file = Path.of(githubOutput);
if (file.getParent() != null) Files.createDirectories(file.getParent());
var lines = (name + "=" + value).lines().toList();
if (lines.size() != 1) {
throw new UnsupportedOperationException("Multiline strings are no supported");
}
debug("Write output %s to %s".formatted(lines, file));
Files.write(file, lines, UTF_8, CREATE, APPEND, WRITE);
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
/** Creates a debug message and prints the message to the log. */
static void debug(String message) {
System.out.printf("::debug::%s%n", message.replaceAll("\\R", "%0A"));
}
/** Creates a warning message and prints the message to the log. */
static void warn(String message) {
System.out.printf("::warning::%s%n", message.replaceAll("\\R", "%0A"));
}
/** Creates an error message and prints the message to the log. */
static void error(String message) {
System.out.printf("::error::%s%n", message.replaceAll("\\R", "%0A"));
}
}
/** HTTP-related helper. */
static class Browser {
final HttpClient client;
Browser() {
this.client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
}
String browse(String uri) throws Exception {
var request = HttpRequest.newBuilder(URI.create(uri)).build();
return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
HttpResponse<Path> download(String uri, Path file) throws Exception {
var parent = file.getParent();
if (parent != null) Files.createDirectories(parent);
var request = HttpRequest.newBuilder(URI.create(uri)).build();
return client.send(request, HttpResponse.BodyHandlers.ofFile(file));
}
HttpResponse<?> head(String uri) throws Exception {
var request =
HttpRequest.newBuilder(URI.create(uri))
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
return client.send(request, HttpResponse.BodyHandlers.discarding());
}
}
/** A website hosting JDK builds. */
interface Website {
/** Try to instantiate a website implementation for the given hint. */
static Optional<Website> find(String hint) {
if (hint.equals(OracleComWebsite.NAME) || hint.startsWith(OracleComWebsite.URI_PREFIX)) {
return Optional.of(new OracleComWebsite());
}
if (hint.equals(JavaNetWebsite.NAME) || hint.startsWith(JavaNetWebsite.URI_PREFIX)) {
return Optional.of(new JavaNetWebsite());
}
return Optional.empty();
}
Optional<String> findUri(JDK jdk);
default Path computeArchivePath(String uri) {
var file = uri.substring(uri.lastIndexOf('/') + 1);
var home = System.getProperty("user.home");
var hash = Integer.toHexString(uri.hashCode());
var cache = Path.of(home, ".oracle-actions", "setup-java", hash);
return cache.resolve(file);
}
default String computeVersionString(String uri, String defaultVersion) {
var property = System.getProperty("install-as-version");
GitHub.debug("install-as-version: " + property);
var version = property == null || property.isBlank() ? defaultVersion : property;
return switch (version) {
case "PARSE_URI" -> parseVersion(uri).orElse("UNKNOWN-VERSION");
case "HASH_URI" -> Integer.toString(Math.abs(uri.hashCode()));
default -> version;
};
}
/** Try to parse version information from the given uri. */
default Optional<String> parseVersion(String uri) {
for (var versionPattern : parseVersionPatterns()) {
var matcher = versionPattern.matcher(uri);
if (matcher.matches()) {
// "$FEATURE.$INTERIM.$UPDATE.$PATCH"
var version = Runtime.Version.parse(matcher.group(1));
var joiner = new StringJoiner(".");
joiner.add(String.valueOf(version.feature()));
if (version.interim() != 0 || version.update() != 0) {
joiner.add(String.valueOf(version.interim()));
if (version.update() != 0) {
joiner.add(String.valueOf(version.update()));
}
}
return Optional.of(joiner.toString());
}
}
return Optional.empty();
}
/** A list of patterns with each has at least one version-defining capture group. */
default List<Pattern> parseVersionPatterns() {
return List.of();
}
/** Test the given uri for potentially pointing to different resources over time. */
default boolean isMovingResourceUri(String uri) {
return false;
}
/** Test the given uri for pointing to an archived JDK build. */
default boolean isArchivedUri(String uri) {
return false;
}
/** The checksum for the given uri, possibly a uri pointing to a remote file. */
default String getChecksum(String uri) {
return uri + ".sha256";
}
}
/** JDK builds hosted at {@code https://oracle.com}. */
static class OracleComWebsite implements Website {
static String NAME = "oracle.com";
static String URI_PREFIX = "https://download.oracle.com/java/";
@Override
public List<Pattern> parseVersionPatterns() {
return List.of(Pattern.compile("\\Q" + URI_PREFIX + "\\E.+?/jdk-([\\d.]+).+"));
}
@Override
public boolean isMovingResourceUri(String uri) {
return uri.contains("/latest/");
}
@Override
public boolean isArchivedUri(String uri) {
return uri.contains("/archive/");
}
@Override
public Optional<String> findUri(JDK jdk) {
if (Integer.parseInt(jdk.release) < 17) return Optional.empty();
if (jdk.version.equals("latest")) return Optional.of(computeLatestUri(jdk));
return Optional.of(computeArchiveUri(jdk));
}
String computeLatestUri(JDK jdk) {
var format = URI_PREFIX + "%s/latest/jdk-%s_%s-%s_bin.%s";
return String.format(format, jdk.release, jdk.release, jdk.os, jdk.arch, jdk.type);
}
String computeArchiveUri(JDK jdk) {
var format = URI_PREFIX + "%s/archive/jdk-%s_%s-%s_bin.%s";
return String.format(format, jdk.release, jdk.version, jdk.os, jdk.arch, jdk.type);
}
}
/** JDK builds hosted at {@code https://jdk.java.net}. */
static class JavaNetWebsite implements Website {
static String NAME = "jdk.java.net";
static String URI_PREFIX = "https://download.java.net";
static /*lazy*/ Properties URI_MAPPING = null;
@Override
public List<Pattern> parseVersionPatterns() {
return List.of(Pattern.compile("\\Q" + URI_PREFIX + "\\E.+?/openjdk-([\\d.]+).+"));
}
@Override
public synchronized Optional<String> findUri(JDK jdk) {
var key =
new StringJoiner(",")
.add(jdk.release)
.add(jdk.version)
.add(jdk.os)
.add(jdk.arch)
.toString();
if (URI_MAPPING == null) {
try {
URI_MAPPING = new Properties();
var browser = new Browser();
var s =
browser.browse(
"https://raw.githubusercontent.com"
+ "/oracle-actions/setup-java/main" // user/repo/branch
+ "/jdk.java.net-uri.properties");
URI_MAPPING.load(new StringReader(s));
} catch (Exception exception) {
GitHub.warn("Caught exception: " + exception);
return Optional.empty();
}
}
return Optional.ofNullable(URI_MAPPING.getProperty(key));
}
}
}