|
1 /* |
|
2 * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. |
|
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
4 * |
|
5 * This code is free software; you can redistribute it and/or modify it |
|
6 * under the terms of the GNU General Public License version 2 only, as |
|
7 * published by the Free Software Foundation. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 |
|
26 package jdk.incubator.jpackage.internal; |
|
27 |
|
28 import java.io.*; |
|
29 import java.net.URI; |
|
30 import java.net.URISyntaxException; |
|
31 import java.nio.file.Files; |
|
32 import java.nio.file.Path; |
|
33 import java.text.MessageFormat; |
|
34 import java.util.*; |
|
35 |
|
36 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; |
|
37 import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN; |
|
38 import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER; |
|
39 import static jdk.incubator.jpackage.internal.MacAppImageBuilder.MAC_CF_BUNDLE_IDENTIFIER; |
|
40 import static jdk.incubator.jpackage.internal.OverridableResource.createResource; |
|
41 |
|
42 public class MacPkgBundler extends MacBaseInstallerBundler { |
|
43 |
|
44 private static final ResourceBundle I18N = ResourceBundle.getBundle( |
|
45 "jdk.incubator.jpackage.internal.resources.MacResources"); |
|
46 |
|
47 private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png"; |
|
48 |
|
49 private static final String TEMPLATE_PREINSTALL_SCRIPT = |
|
50 "preinstall.template"; |
|
51 private static final String TEMPLATE_POSTINSTALL_SCRIPT = |
|
52 "postinstall.template"; |
|
53 |
|
54 private static final BundlerParamInfo<File> PACKAGES_ROOT = |
|
55 new StandardBundlerParam<>( |
|
56 "mac.pkg.packagesRoot", |
|
57 File.class, |
|
58 params -> { |
|
59 File packagesRoot = |
|
60 new File(TEMP_ROOT.fetchFrom(params), "packages"); |
|
61 packagesRoot.mkdirs(); |
|
62 return packagesRoot; |
|
63 }, |
|
64 (s, p) -> new File(s)); |
|
65 |
|
66 |
|
67 protected final BundlerParamInfo<File> SCRIPTS_DIR = |
|
68 new StandardBundlerParam<>( |
|
69 "mac.pkg.scriptsDir", |
|
70 File.class, |
|
71 params -> { |
|
72 File scriptsDir = |
|
73 new File(CONFIG_ROOT.fetchFrom(params), "scripts"); |
|
74 scriptsDir.mkdirs(); |
|
75 return scriptsDir; |
|
76 }, |
|
77 (s, p) -> new File(s)); |
|
78 |
|
79 public static final |
|
80 BundlerParamInfo<String> DEVELOPER_ID_INSTALLER_SIGNING_KEY = |
|
81 new StandardBundlerParam<>( |
|
82 "mac.signing-key-developer-id-installer", |
|
83 String.class, |
|
84 params -> { |
|
85 String result = MacBaseInstallerBundler.findKey( |
|
86 "Developer ID Installer: " |
|
87 + SIGNING_KEY_USER.fetchFrom(params), |
|
88 SIGNING_KEYCHAIN.fetchFrom(params), |
|
89 VERBOSE.fetchFrom(params)); |
|
90 if (result != null) { |
|
91 MacCertificate certificate = new MacCertificate(result); |
|
92 |
|
93 if (!certificate.isValid()) { |
|
94 Log.error(MessageFormat.format( |
|
95 I18N.getString("error.certificate.expired"), |
|
96 result)); |
|
97 } |
|
98 } |
|
99 |
|
100 return result; |
|
101 }, |
|
102 (s, p) -> s); |
|
103 |
|
104 public static final BundlerParamInfo<String> MAC_INSTALL_DIR = |
|
105 new StandardBundlerParam<>( |
|
106 "mac-install-dir", |
|
107 String.class, |
|
108 params -> { |
|
109 String dir = INSTALL_DIR.fetchFrom(params); |
|
110 return (dir != null) ? dir : "/Applications"; |
|
111 }, |
|
112 (s, p) -> s |
|
113 ); |
|
114 |
|
115 public static final BundlerParamInfo<String> INSTALLER_SUFFIX = |
|
116 new StandardBundlerParam<> ( |
|
117 "mac.pkg.installerName.suffix", |
|
118 String.class, |
|
119 params -> "", |
|
120 (s, p) -> s); |
|
121 |
|
122 public File bundle(Map<String, ? super Object> params, |
|
123 File outdir) throws PackagerException { |
|
124 Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"), |
|
125 APP_NAME.fetchFrom(params))); |
|
126 |
|
127 IOUtils.writableOutputDir(outdir.toPath()); |
|
128 |
|
129 try { |
|
130 File appImageDir = prepareAppBundle(params); |
|
131 |
|
132 if (appImageDir != null && prepareConfigFiles(params)) { |
|
133 |
|
134 File configScript = getConfig_Script(params); |
|
135 if (configScript.exists()) { |
|
136 Log.verbose(MessageFormat.format(I18N.getString( |
|
137 "message.running-script"), |
|
138 configScript.getAbsolutePath())); |
|
139 IOUtils.run("bash", configScript); |
|
140 } |
|
141 |
|
142 return createPKG(params, outdir, appImageDir); |
|
143 } |
|
144 return null; |
|
145 } catch (IOException ex) { |
|
146 Log.verbose(ex); |
|
147 throw new PackagerException(ex); |
|
148 } |
|
149 } |
|
150 |
|
151 private File getPackages_AppPackage(Map<String, ? super Object> params) { |
|
152 return new File(PACKAGES_ROOT.fetchFrom(params), |
|
153 APP_NAME.fetchFrom(params) + "-app.pkg"); |
|
154 } |
|
155 |
|
156 private File getConfig_DistributionXMLFile( |
|
157 Map<String, ? super Object> params) { |
|
158 return new File(CONFIG_ROOT.fetchFrom(params), "distribution.dist"); |
|
159 } |
|
160 |
|
161 private File getConfig_BackgroundImage(Map<String, ? super Object> params) { |
|
162 return new File(CONFIG_ROOT.fetchFrom(params), |
|
163 APP_NAME.fetchFrom(params) + "-background.png"); |
|
164 } |
|
165 |
|
166 private File getConfig_BackgroundImageDarkAqua(Map<String, ? super Object> params) { |
|
167 return new File(CONFIG_ROOT.fetchFrom(params), |
|
168 APP_NAME.fetchFrom(params) + "-background-darkAqua.png"); |
|
169 } |
|
170 |
|
171 private File getScripts_PreinstallFile(Map<String, ? super Object> params) { |
|
172 return new File(SCRIPTS_DIR.fetchFrom(params), "preinstall"); |
|
173 } |
|
174 |
|
175 private File getScripts_PostinstallFile( |
|
176 Map<String, ? super Object> params) { |
|
177 return new File(SCRIPTS_DIR.fetchFrom(params), "postinstall"); |
|
178 } |
|
179 |
|
180 private String getAppIdentifier(Map<String, ? super Object> params) { |
|
181 return MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); |
|
182 } |
|
183 |
|
184 private void preparePackageScripts(Map<String, ? super Object> params) |
|
185 throws IOException { |
|
186 Log.verbose(I18N.getString("message.preparing-scripts")); |
|
187 |
|
188 Map<String, String> data = new HashMap<>(); |
|
189 |
|
190 Path appLocation = Path.of(MAC_INSTALL_DIR.fetchFrom(params), |
|
191 APP_NAME.fetchFrom(params) + ".app", "Contents", "app"); |
|
192 |
|
193 data.put("INSTALL_LOCATION", MAC_INSTALL_DIR.fetchFrom(params)); |
|
194 data.put("APP_LOCATION", appLocation.toString()); |
|
195 |
|
196 createResource(TEMPLATE_PREINSTALL_SCRIPT, params) |
|
197 .setCategory(I18N.getString("resource.pkg-preinstall-script")) |
|
198 .setSubstitutionData(data) |
|
199 .saveToFile(getScripts_PreinstallFile(params)); |
|
200 getScripts_PreinstallFile(params).setExecutable(true, false); |
|
201 |
|
202 createResource(TEMPLATE_POSTINSTALL_SCRIPT, params) |
|
203 .setCategory(I18N.getString("resource.pkg-postinstall-script")) |
|
204 .setSubstitutionData(data) |
|
205 .saveToFile(getScripts_PostinstallFile(params)); |
|
206 getScripts_PostinstallFile(params).setExecutable(true, false); |
|
207 } |
|
208 |
|
209 private static String URLEncoding(String pkgName) throws URISyntaxException { |
|
210 URI uri = new URI(null, null, pkgName, null); |
|
211 return uri.toASCIIString(); |
|
212 } |
|
213 |
|
214 private void prepareDistributionXMLFile(Map<String, ? super Object> params) |
|
215 throws IOException { |
|
216 File f = getConfig_DistributionXMLFile(params); |
|
217 |
|
218 Log.verbose(MessageFormat.format(I18N.getString( |
|
219 "message.preparing-distribution-dist"), f.getAbsolutePath())); |
|
220 |
|
221 IOUtils.createXml(f.toPath(), xml -> { |
|
222 xml.writeStartElement("installer-gui-script"); |
|
223 xml.writeAttribute("minSpecVersion", "1"); |
|
224 |
|
225 xml.writeStartElement("title"); |
|
226 xml.writeCharacters(APP_NAME.fetchFrom(params)); |
|
227 xml.writeEndElement(); |
|
228 |
|
229 xml.writeStartElement("background"); |
|
230 xml.writeAttribute("file", getConfig_BackgroundImage(params).getName()); |
|
231 xml.writeAttribute("mime-type", "image/png"); |
|
232 xml.writeAttribute("alignment", "bottomleft"); |
|
233 xml.writeAttribute("scaling", "none"); |
|
234 xml.writeEndElement(); |
|
235 |
|
236 xml.writeStartElement("background-darkAqua"); |
|
237 xml.writeAttribute("file", getConfig_BackgroundImageDarkAqua(params).getName()); |
|
238 xml.writeAttribute("mime-type", "image/png"); |
|
239 xml.writeAttribute("alignment", "bottomleft"); |
|
240 xml.writeAttribute("scaling", "none"); |
|
241 xml.writeEndElement(); |
|
242 |
|
243 String licFileStr = LICENSE_FILE.fetchFrom(params); |
|
244 if (licFileStr != null) { |
|
245 File licFile = new File(licFileStr); |
|
246 xml.writeStartElement("license"); |
|
247 xml.writeAttribute("file", licFile.getAbsolutePath()); |
|
248 xml.writeAttribute("mime-type", "text/rtf"); |
|
249 xml.writeEndElement(); |
|
250 } |
|
251 |
|
252 /* |
|
253 * Note that the content of the distribution file |
|
254 * below is generated by productbuild --synthesize |
|
255 */ |
|
256 String appId = getAppIdentifier(params); |
|
257 |
|
258 xml.writeStartElement("pkg-ref"); |
|
259 xml.writeAttribute("id", appId); |
|
260 xml.writeEndElement(); // </pkg-ref> |
|
261 xml.writeStartElement("options"); |
|
262 xml.writeAttribute("customize", "never"); |
|
263 xml.writeAttribute("require-scripts", "false"); |
|
264 xml.writeEndElement(); // </options> |
|
265 xml.writeStartElement("choices-outline"); |
|
266 xml.writeStartElement("line"); |
|
267 xml.writeAttribute("choice", "default"); |
|
268 xml.writeStartElement("line"); |
|
269 xml.writeAttribute("choice", appId); |
|
270 xml.writeEndElement(); // </line> |
|
271 xml.writeEndElement(); // </line> |
|
272 xml.writeEndElement(); // </choices-outline> |
|
273 xml.writeStartElement("choice"); |
|
274 xml.writeAttribute("id", "default"); |
|
275 xml.writeEndElement(); // </choice> |
|
276 xml.writeStartElement("choice"); |
|
277 xml.writeAttribute("id", appId); |
|
278 xml.writeAttribute("visible", "false"); |
|
279 xml.writeStartElement("pkg-ref"); |
|
280 xml.writeAttribute("id", appId); |
|
281 xml.writeEndElement(); // </pkg-ref> |
|
282 xml.writeEndElement(); // </choice> |
|
283 xml.writeStartElement("pkg-ref"); |
|
284 xml.writeAttribute("id", appId); |
|
285 xml.writeAttribute("version", VERSION.fetchFrom(params)); |
|
286 xml.writeAttribute("onConclusion", "none"); |
|
287 try { |
|
288 xml.writeCharacters(URLEncoding( |
|
289 getPackages_AppPackage(params).getName())); |
|
290 } catch (URISyntaxException ex) { |
|
291 throw new IOException(ex); |
|
292 } |
|
293 xml.writeEndElement(); // </pkg-ref> |
|
294 |
|
295 xml.writeEndElement(); // </installer-gui-script> |
|
296 }); |
|
297 } |
|
298 |
|
299 private boolean prepareConfigFiles(Map<String, ? super Object> params) |
|
300 throws IOException { |
|
301 |
|
302 createResource(DEFAULT_BACKGROUND_IMAGE, params) |
|
303 .setCategory(I18N.getString("resource.pkg-background-image")) |
|
304 .saveToFile(getConfig_BackgroundImage(params)); |
|
305 |
|
306 createResource(DEFAULT_BACKGROUND_IMAGE, params) |
|
307 .setCategory(I18N.getString("resource.pkg-background-image")) |
|
308 .saveToFile(getConfig_BackgroundImageDarkAqua(params)); |
|
309 |
|
310 prepareDistributionXMLFile(params); |
|
311 |
|
312 createResource(null, params) |
|
313 .setCategory(I18N.getString("resource.post-install-script")) |
|
314 .saveToFile(getConfig_Script(params)); |
|
315 |
|
316 return true; |
|
317 } |
|
318 |
|
319 // name of post-image script |
|
320 private File getConfig_Script(Map<String, ? super Object> params) { |
|
321 return new File(CONFIG_ROOT.fetchFrom(params), |
|
322 APP_NAME.fetchFrom(params) + "-post-image.sh"); |
|
323 } |
|
324 |
|
325 private void patchCPLFile(File cpl) throws IOException { |
|
326 String cplData = Files.readString(cpl.toPath()); |
|
327 String[] lines = cplData.split("\n"); |
|
328 try (PrintWriter out = new PrintWriter(Files.newBufferedWriter( |
|
329 cpl.toPath()))) { |
|
330 int skip = 0; |
|
331 // Used to skip Java.runtime bundle, since |
|
332 // pkgbuild with --root will find two bundles app and Java runtime. |
|
333 // We cannot generate component proprty list when using |
|
334 // --component argument. |
|
335 for (int i = 0; i < lines.length; i++) { |
|
336 if (lines[i].trim().equals("<key>BundleIsRelocatable</key>")) { |
|
337 out.println(lines[i]); |
|
338 out.println("<false/>"); |
|
339 i++; |
|
340 } else if (lines[i].trim().equals("<key>ChildBundles</key>")) { |
|
341 ++skip; |
|
342 } else if ((skip > 0) && lines[i].trim().equals("</array>")) { |
|
343 --skip; |
|
344 } else { |
|
345 if (skip == 0) { |
|
346 out.println(lines[i]); |
|
347 } |
|
348 } |
|
349 } |
|
350 } |
|
351 } |
|
352 |
|
353 // pkgbuild includes all components from "--root" and subfolders, |
|
354 // so if we have app image in folder which contains other images, then they |
|
355 // will be included as well. It does have "--filter" option which use regex |
|
356 // to exclude files/folder, but it will overwrite default one which excludes |
|
357 // based on doc "any .svn or CVS directories, and any .DS_Store files". |
|
358 // So easy aproach will be to copy user provided app-image into temp folder |
|
359 // if root path contains other files. |
|
360 private String getRoot(Map<String, ? super Object> params, |
|
361 File appLocation) throws IOException { |
|
362 String root = appLocation.getParent() == null ? |
|
363 "." : appLocation.getParent(); |
|
364 File rootDir = new File(root); |
|
365 File[] list = rootDir.listFiles(); |
|
366 if (list != null) { // Should not happend |
|
367 // We should only have app image and/or .DS_Store |
|
368 if (list.length == 1) { |
|
369 return root; |
|
370 } else if (list.length == 2) { |
|
371 // Check case with app image and .DS_Store |
|
372 if (list[0].toString().toLowerCase().endsWith(".ds_store") || |
|
373 list[1].toString().toLowerCase().endsWith(".ds_store")) { |
|
374 return root; // Only app image and .DS_Store |
|
375 } |
|
376 } |
|
377 } |
|
378 |
|
379 // Copy to new root |
|
380 Path newRoot = Files.createTempDirectory( |
|
381 TEMP_ROOT.fetchFrom(params).toPath(), |
|
382 "root-"); |
|
383 |
|
384 IOUtils.copyRecursive(appLocation.toPath(), |
|
385 newRoot.resolve(appLocation.getName())); |
|
386 |
|
387 return newRoot.toString(); |
|
388 } |
|
389 |
|
390 private File createPKG(Map<String, ? super Object> params, |
|
391 File outdir, File appLocation) { |
|
392 // generic find attempt |
|
393 try { |
|
394 File appPKG = getPackages_AppPackage(params); |
|
395 |
|
396 String root = getRoot(params, appLocation); |
|
397 |
|
398 // Generate default CPL file |
|
399 File cpl = new File(CONFIG_ROOT.fetchFrom(params).getAbsolutePath() |
|
400 + File.separator + "cpl.plist"); |
|
401 ProcessBuilder pb = new ProcessBuilder("pkgbuild", |
|
402 "--root", |
|
403 root, |
|
404 "--install-location", |
|
405 MAC_INSTALL_DIR.fetchFrom(params), |
|
406 "--analyze", |
|
407 cpl.getAbsolutePath()); |
|
408 |
|
409 IOUtils.exec(pb); |
|
410 |
|
411 patchCPLFile(cpl); |
|
412 |
|
413 preparePackageScripts(params); |
|
414 |
|
415 // build application package |
|
416 pb = new ProcessBuilder("pkgbuild", |
|
417 "--root", |
|
418 root, |
|
419 "--install-location", |
|
420 MAC_INSTALL_DIR.fetchFrom(params), |
|
421 "--component-plist", |
|
422 cpl.getAbsolutePath(), |
|
423 "--scripts", |
|
424 SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(), |
|
425 appPKG.getAbsolutePath()); |
|
426 IOUtils.exec(pb); |
|
427 |
|
428 // build final package |
|
429 File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params) |
|
430 + INSTALLER_SUFFIX.fetchFrom(params) |
|
431 + ".pkg"); |
|
432 outdir.mkdirs(); |
|
433 |
|
434 List<String> commandLine = new ArrayList<>(); |
|
435 commandLine.add("productbuild"); |
|
436 |
|
437 commandLine.add("--resources"); |
|
438 commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); |
|
439 |
|
440 // maybe sign |
|
441 if (Optional.ofNullable(MacAppImageBuilder. |
|
442 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { |
|
443 if (Platform.getMajorVersion() > 10 || |
|
444 (Platform.getMajorVersion() == 10 && |
|
445 Platform.getMinorVersion() >= 12)) { |
|
446 // we need this for OS X 10.12+ |
|
447 Log.verbose(I18N.getString("message.signing.pkg")); |
|
448 } |
|
449 |
|
450 String signingIdentity = |
|
451 DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); |
|
452 if (signingIdentity != null) { |
|
453 commandLine.add("--sign"); |
|
454 commandLine.add(signingIdentity); |
|
455 } |
|
456 |
|
457 String keychainName = SIGNING_KEYCHAIN.fetchFrom(params); |
|
458 if (keychainName != null && !keychainName.isEmpty()) { |
|
459 commandLine.add("--keychain"); |
|
460 commandLine.add(keychainName); |
|
461 } |
|
462 } |
|
463 |
|
464 commandLine.add("--distribution"); |
|
465 commandLine.add( |
|
466 getConfig_DistributionXMLFile(params).getAbsolutePath()); |
|
467 commandLine.add("--package-path"); |
|
468 commandLine.add(PACKAGES_ROOT.fetchFrom(params).getAbsolutePath()); |
|
469 |
|
470 commandLine.add(finalPKG.getAbsolutePath()); |
|
471 |
|
472 pb = new ProcessBuilder(commandLine); |
|
473 IOUtils.exec(pb); |
|
474 |
|
475 return finalPKG; |
|
476 } catch (Exception ignored) { |
|
477 Log.verbose(ignored); |
|
478 return null; |
|
479 } |
|
480 } |
|
481 |
|
482 ////////////////////////////////////////////////////////////////////////// |
|
483 // Implement Bundler |
|
484 ////////////////////////////////////////////////////////////////////////// |
|
485 |
|
486 @Override |
|
487 public String getName() { |
|
488 return I18N.getString("pkg.bundler.name"); |
|
489 } |
|
490 |
|
491 @Override |
|
492 public String getID() { |
|
493 return "pkg"; |
|
494 } |
|
495 |
|
496 @Override |
|
497 public boolean validate(Map<String, ? super Object> params) |
|
498 throws ConfigException { |
|
499 try { |
|
500 Objects.requireNonNull(params); |
|
501 |
|
502 // run basic validation to ensure requirements are met |
|
503 // we are not interested in return code, only possible exception |
|
504 validateAppImageAndBundeler(params); |
|
505 |
|
506 if (MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) == null) { |
|
507 throw new ConfigException( |
|
508 I18N.getString("message.app-image-requires-identifier"), |
|
509 I18N.getString( |
|
510 "message.app-image-requires-identifier.advice")); |
|
511 } |
|
512 |
|
513 // reject explicitly set sign to true and no valid signature key |
|
514 if (Optional.ofNullable(MacAppImageBuilder. |
|
515 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { |
|
516 String signingIdentity = |
|
517 DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); |
|
518 if (signingIdentity == null) { |
|
519 throw new ConfigException( |
|
520 I18N.getString("error.explicit-sign-no-cert"), |
|
521 I18N.getString( |
|
522 "error.explicit-sign-no-cert.advice")); |
|
523 } |
|
524 } |
|
525 |
|
526 // hdiutil is always available so there's no need |
|
527 // to test for availability. |
|
528 |
|
529 return true; |
|
530 } catch (RuntimeException re) { |
|
531 if (re.getCause() instanceof ConfigException) { |
|
532 throw (ConfigException) re.getCause(); |
|
533 } else { |
|
534 throw new ConfigException(re); |
|
535 } |
|
536 } |
|
537 } |
|
538 |
|
539 @Override |
|
540 public File execute(Map<String, ? super Object> params, |
|
541 File outputParentDir) throws PackagerException { |
|
542 return bundle(params, outputParentDir); |
|
543 } |
|
544 |
|
545 @Override |
|
546 public boolean supported(boolean runtimeInstaller) { |
|
547 return true; |
|
548 } |
|
549 |
|
550 @Override |
|
551 public boolean isDefault() { |
|
552 return false; |
|
553 } |
|
554 |
|
555 } |