|
| 1 | +package io.github.javapackager; |
| 2 | + |
| 3 | +import java.io.File; |
| 4 | +import java.io.IOException; |
| 5 | +import java.io.StringWriter; |
| 6 | +import java.nio.charset.StandardCharsets; |
| 7 | +import java.nio.file.Files; |
| 8 | +import java.nio.file.Path; |
| 9 | +import java.nio.file.Paths; |
| 10 | +import java.util.ArrayList; |
| 11 | +import java.util.Arrays; |
| 12 | +import java.util.Collections; |
| 13 | +import java.util.HashMap; |
| 14 | +import java.util.List; |
| 15 | +import java.util.Map; |
| 16 | +import java.util.stream.Collectors; |
| 17 | +import java.util.stream.Stream; |
| 18 | + |
| 19 | +import org.apache.velocity.Template; |
| 20 | +import org.apache.velocity.VelocityContext; |
| 21 | +import org.apache.velocity.app.VelocityEngine; |
| 22 | +import org.apache.velocity.runtime.RuntimeConstants; |
| 23 | +import org.apache.velocity.runtime.resource.loader.FileResourceLoader; |
| 24 | + |
| 25 | +/** |
| 26 | + * Checks the plugin's Velocity templates (run by the checkTemplates Gradle task): |
| 27 | + * every template must parse, and the shell templates, rendered with sample settings, |
| 28 | + * must be valid Bash. The Bash checks are skipped on Windows. |
| 29 | + * <p> |
| 30 | + * Usage: TemplateCheck <templates folder> <output folder> |
| 31 | + */ |
| 32 | +public class TemplateCheck { |
| 33 | + |
| 34 | + private static final List<String> failures = new ArrayList<>(); |
| 35 | + |
| 36 | + public static void main(String[] args) throws Exception { |
| 37 | + File templatesDir = new File(args[0]); |
| 38 | + File outputDir = new File(args[1]); |
| 39 | + outputDir.mkdirs(); |
| 40 | + |
| 41 | + VelocityEngine engine = new VelocityEngine(); |
| 42 | + engine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "file"); |
| 43 | + engine.setProperty("resource.loader.file.class", FileResourceLoader.class.getName()); |
| 44 | + engine.setProperty("resource.loader.file.path", templatesDir.getAbsolutePath()); |
| 45 | + engine.init(); |
| 46 | + |
| 47 | + List<String> templates; |
| 48 | + try (Stream<Path> files = Files.walk(templatesDir.toPath())) { |
| 49 | + templates = files |
| 50 | + .filter(p -> p.toString().endsWith(".vtl")) |
| 51 | + .map(p -> templatesDir.toPath().relativize(p).toString().replace('\\', '/')) |
| 52 | + .sorted() |
| 53 | + .collect(Collectors.toList()); |
| 54 | + } |
| 55 | + |
| 56 | + // 1. every template parses |
| 57 | + for (String template : templates) { |
| 58 | + try { |
| 59 | + engine.getTemplate(template, "UTF-8"); |
| 60 | + System.out.println("PASS: parses: " + template); |
| 61 | + } catch (Exception e) { |
| 62 | + fail(template + " doesn't parse: " + e.getMessage()); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + // 2. shell templates are valid Bash with different settings |
| 67 | + boolean bash = !System.getProperty("os.name").toLowerCase().startsWith("windows"); |
| 68 | + if (!bash) { |
| 69 | + System.out.println("SKIP: Bash checks (Windows)"); |
| 70 | + } |
| 71 | + for (Map<String, Object> info : linuxVariants()) { |
| 72 | + File script = render(engine, "linux/startup.sh.vtl", info, new File(outputDir, "startup-" + info.get("variant") + ".sh")); |
| 73 | + if (bash && script != null) { |
| 74 | + bashSyntax(script); |
| 75 | + if (info.get("jreMinVersion") != null) { |
| 76 | + compareVersionsTests(script, outputDir); |
| 77 | + } |
| 78 | + } |
| 79 | + } |
| 80 | + for (Map<String, Object> info : macVariants()) { |
| 81 | + File script = render(engine, "mac/startup.vtl", info, new File(outputDir, "mac-startup-" + info.get("variant") + ".sh")); |
| 82 | + if (bash && script != null) { |
| 83 | + bashSyntax(script); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + if (!failures.isEmpty()) { |
| 88 | + System.err.println(failures.size() + " template check(s) failed:"); |
| 89 | + failures.forEach(f -> System.err.println(" " + f)); |
| 90 | + System.exit(1); |
| 91 | + } |
| 92 | + System.out.println("All template checks passed"); |
| 93 | + } |
| 94 | + |
| 95 | + private static List<Map<String, Object>> linuxVariants() { |
| 96 | + // bundled JRE, default settings |
| 97 | + Map<String, Object> bundled = linuxInfo("bundled-jre"); |
| 98 | + // system Java with a minimum version, administrator, classpath, bootstrap script, envPath, no wrapped JAR |
| 99 | + Map<String, Object> full = linuxInfo("system-java"); |
| 100 | + full.put("bundleJre", false); |
| 101 | + full.put("jreMinVersion", "11"); |
| 102 | + full.put("administratorRequired", true); |
| 103 | + full.put("classpath", "extra:lib/other.jar"); |
| 104 | + full.put("classpaths", Arrays.asList("extra", "lib/other.jar")); |
| 105 | + full.put("envPath", "/usr/local/bin:$PATH"); |
| 106 | + full.put("scripts", map("bootstrap", "bootstrap.sh")); |
| 107 | + full.put("bootstrapFile", map("name", "bootstrap.sh")); |
| 108 | + full.put("linuxConfig", map("wrapJar", false)); |
| 109 | + full.put("useResourcesAsWorkingDir", false); |
| 110 | + return Arrays.asList(bundled, full); |
| 111 | + } |
| 112 | + |
| 113 | + private static Map<String, Object> linuxInfo(String variant) { |
| 114 | + Map<String, Object> info = new HashMap<>(); |
| 115 | + info.put("variant", variant); |
| 116 | + info.put("name", "HelloWorld"); |
| 117 | + info.put("mainClass", "com.example.Main"); |
| 118 | + info.put("bundleJre", true); |
| 119 | + info.put("jreDirectoryName", "jre"); |
| 120 | + info.put("administratorRequired", false); |
| 121 | + info.put("useResourcesAsWorkingDir", true); |
| 122 | + info.put("linuxConfig", map("wrapJar", true)); |
| 123 | + info.put("executable", map("name", "HelloWorld")); |
| 124 | + info.put("jarFile", map("name", "HelloWorld-1.0.0-runnable.jar")); |
| 125 | + info.put("vmArgs", Arrays.asList("-Xmx1g", "-Dapp.title=Hello World")); |
| 126 | + info.put("appArgs", Arrays.asList("--foo", "hello world")); |
| 127 | + info.put("classpaths", Collections.emptyList()); |
| 128 | + info.put("scripts", new HashMap<>()); |
| 129 | + return info; |
| 130 | + } |
| 131 | + |
| 132 | + private static List<Map<String, Object>> macVariants() { |
| 133 | + Map<String, Object> info = new HashMap<>(); |
| 134 | + info.put("variant", "admin"); |
| 135 | + info.put("name", "HelloWorld"); |
| 136 | + info.put("launcher", map("name", "universalJavaApplicationStub")); |
| 137 | + return Collections.singletonList(info); |
| 138 | + } |
| 139 | + |
| 140 | + private static Map<String, Object> map(String key, Object value) { |
| 141 | + Map<String, Object> map = new HashMap<>(); |
| 142 | + map.put(key, value); |
| 143 | + return map; |
| 144 | + } |
| 145 | + |
| 146 | + private static File render(VelocityEngine engine, String templatePath, Map<String, Object> info, File output) { |
| 147 | + try { |
| 148 | + VelocityContext context = new VelocityContext(); |
| 149 | + context.put("info", info); |
| 150 | + Template template = engine.getTemplate(templatePath, "UTF-8"); |
| 151 | + StringWriter writer = new StringWriter(); |
| 152 | + template.merge(context, writer); |
| 153 | + Files.write(output.toPath(), writer.toString().replace("\r\n", "\n").getBytes(StandardCharsets.UTF_8)); |
| 154 | + System.out.println("PASS: renders: " + templatePath + " (" + info.get("variant") + ")"); |
| 155 | + return output; |
| 156 | + } catch (Exception e) { |
| 157 | + fail(templatePath + " (" + info.get("variant") + ") doesn't render: " + e.getMessage()); |
| 158 | + return null; |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + private static void bashSyntax(File script) throws IOException, InterruptedException { |
| 163 | + Result result = run("bash", "-n", script.getAbsolutePath()); |
| 164 | + if (result.code == 0) { |
| 165 | + System.out.println("PASS: valid Bash: " + script.getName()); |
| 166 | + } else { |
| 167 | + fail(script.getName() + " is not valid Bash: " + result.output); |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * Runs the startup script's compareVersions() with known Java and minimum versions (#505) |
| 173 | + */ |
| 174 | + private static void compareVersionsTests(File script, File outputDir) throws IOException, InterruptedException { |
| 175 | + String content = new String(Files.readAllBytes(script.toPath()), StandardCharsets.UTF_8); |
| 176 | + int start = content.indexOf("function compareVersions()"); |
| 177 | + int end = content.indexOf("\n}\n", start); |
| 178 | + if (start < 0 || end < 0) { |
| 179 | + fail(script.getName() + ": compareVersions() not found"); |
| 180 | + return; |
| 181 | + } |
| 182 | + String[][] cases = { |
| 183 | + // java version, minimum version, expected result (0 = enough, 1 = too old) |
| 184 | + { "17.0.9", "8", "0" }, |
| 185 | + { "9", "11", "1" }, |
| 186 | + { "1.8.0_392", "8", "0" }, |
| 187 | + { "1.8.0_392", "11", "1" }, |
| 188 | + { "11.0.2", "11", "0" }, |
| 189 | + { "21", "17.0.1", "0" }, |
| 190 | + { "17-ea", "17", "0" }, |
| 191 | + { "17.0.9", "17.0.10", "1" }, |
| 192 | + }; |
| 193 | + StringBuilder test = new StringBuilder(content.substring(start, end + 3)); |
| 194 | + test.append("failed=0\n"); |
| 195 | + for (String[] c : cases) { |
| 196 | + test.append("compareVersions '").append(c[0]).append("' '").append(c[1]).append("'; r=$?; ") |
| 197 | + .append("[ \"$r\" = ").append(c[2]).append(" ] || { echo 'java ").append(c[0]).append(" vs ").append(c[1]) |
| 198 | + .append(": got '$r', expected ").append(c[2]).append("'; failed=1; }\n"); |
| 199 | + } |
| 200 | + test.append("exit $failed\n"); |
| 201 | + Path testFile = Paths.get(outputDir.getAbsolutePath(), "compare-versions-test.sh"); |
| 202 | + Files.write(testFile, test.toString().getBytes(StandardCharsets.UTF_8)); |
| 203 | + Result result = run("bash", testFile.toString()); |
| 204 | + if (result.code == 0) { |
| 205 | + System.out.println("PASS: compareVersions() gives the expected results (" + cases.length + " cases)"); |
| 206 | + } else { |
| 207 | + fail("compareVersions() gives wrong results: " + result.output); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + private static Result run(String... command) throws IOException, InterruptedException { |
| 212 | + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); |
| 213 | + String output = new String(readAll(process), StandardCharsets.UTF_8).trim(); |
| 214 | + return new Result(process.waitFor(), output); |
| 215 | + } |
| 216 | + |
| 217 | + private static byte[] readAll(Process process) throws IOException { |
| 218 | + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); |
| 219 | + byte[] buffer = new byte[4096]; |
| 220 | + int n; |
| 221 | + while ((n = process.getInputStream().read(buffer)) != -1) { |
| 222 | + out.write(buffer, 0, n); |
| 223 | + } |
| 224 | + return out.toByteArray(); |
| 225 | + } |
| 226 | + |
| 227 | + private static void fail(String message) { |
| 228 | + System.out.println("FAIL: " + message); |
| 229 | + failures.add(message); |
| 230 | + } |
| 231 | + |
| 232 | + private static class Result { |
| 233 | + final int code; |
| 234 | + final String output; |
| 235 | + Result(int code, String output) { |
| 236 | + this.code = code; |
| 237 | + this.output = output; |
| 238 | + } |
| 239 | + } |
| 240 | + |
| 241 | +} |
0 commit comments