Итак, в прошлый раз я писал об инструменте для сборки приложений JavaFXPackager. Там было 2 каких-то способа собрать приложение, но ни один из них не мог быть удобно вызван просто из кода. Но мы же труЪ Java-программисты. И вот для таких труЪ-программистов с версии 8u20 и был создан в JDK специальный API в JavaFXPackager, который позволяет просто вот так взять и собрать бандл из ваших бинарников. Одна проблема — этот API незадокументирован. Но не беда, разберёмся.
Первичным источником будет, естественно, JavaDoc, который, впрочем, тоже не доступен публично. Поэтому мы соберём его сами из исходников. Не заставляя это делать читателей, я просто выложу его: ginz.bitbucket.org/fxpackager-javadoc. Конечно, нелишним будет иметь у себя исходный код, так как документирован API плохо.
Какой класс заставляет в первую очередь обратить на себя внимание? Видимо, интерфейс Bundler с вот такой спецификацией:
Методы явно делятся на 2 вида: методы, производящие действия с уже данными параметрами и методы, дающие информацию об этом бандлере.
execute принимает Map параметров и выходную директорию в качестве параметров и создаёт в данной директории бандл в каком-то ожидаемом формате. Но ожидать правильного исполнения мы можем только если validate проходит без исключений, а их он может кинуть 2 вида: UnsupportedPlatformException и ConfigException, говорящие названия которых подсказывают, что в случае выкидывания первого вы просто используете бандлер, который не поддерживается вашей платформой. Второй же выкидывается, если у вас что-то не так с переданными параметрами.
А что вообще можно передавать в качестве параметров в эти методы? Большая часть ключей в этих методах находится во всяких статических экземплярах класса BundlerParamInfo, большая часть из которых (общие кроссплатформенные параметры) находятся в классе StandardBundlerParam, а более специфичные (платформенно-зависимые), как правило, находятся в самих классах-реализациях Bundler'а: LinuxAppBundler, WinExeBundler и так далее. Хотя не совсем очевидно, каким образом этот класс вообще связан с тем, что мы кладём в params, который передаём execute. На самом деле оказывается, что id, получаемый getID(), является ключом в этом Map'е, а значение должно быть типа, которым параметризован BundlerParamInfo.
Самый простой метод получения всех возможных «предустановленных» Bundler'ов через статический метод createBundlersInstance интерфейса Bundlers (Warning, HOT: Java 8):
Таким образом, мы не только получили все «предустановленные» бандлеры, но и отфильтровали их по платформенному принципу: как правило, невозможно собирать пакеты для одной платформы на другой.
А, точно, можно попробовать перестать теоретизировать и нарисовать какой-нибудь пример.
Поскольку мне кажется, что статическая типизация всегда лучше, то мы можем обернуть создание ассоциативного массива параметров во что-то типа BunderParamsBuilder:
Таким образом, сборка проекта будет заключаться примерно в следующем (подразумевается существование запускающегося /tmp/helloWorld.jar):
Выхлоп после запуска будет примерно следующий:
Ой, откуда у нас всё это тут взялось? Мы вроде клали в params только 2 параметра. Чтобы разобраться в этом, надо посмотреть на сорцы метода BunderParamInfo.fetchFrom, который используется для того, чтобы получить значение параметра из params:
Ага, если параметр не был найден в params и он может быть получен через другие параметры (за это отвечает функция defaultValueFunction), то значение, полученное таким образом, запихивается в params. Например, мы не указали параметр MAIN_JAR, но MAIN_JAR обязателен для того, чтобы создать запускаемый файл. Посмотрим на то, как определяется MAIN_JAR: в качестве defaultValueFunction видим:
Вот и ответ, откуда там появляется значение mainJar.
Для того, чтобы не искать каждый раз параметры, я сделал табличку, в которую вывел информацию по всем найденным статическим экземплярам BundlerParamInfo.
Описан самый минимум и самые очевидные возможности, так что не стесняйтесь читать JavaDoc и даже код.
Статья будет улучшаться и исправляться по запросу комментаторов.
Источники информации
Первичным источником будет, естественно, JavaDoc, который, впрочем, тоже не доступен публично. Поэтому мы соберём его сами из исходников. Не заставляя это делать читателей, я просто выложу его: ginz.bitbucket.org/fxpackager-javadoc. Конечно, нелишним будет иметь у себя исходный код, так как документирован API плохо.
Введение
Какой класс заставляет в первую очередь обратить на себя внимание? Видимо, интерфейс Bundler с вот такой спецификацией:
// action methods
File execute(Map<String,? super Object> params, File outputParentDir);
boolean validate(Map<String,? super Object> params);
// information methods
Collection<BundlerParamInfo<?>> getBundleParameters();
String getBundleType();
String getDescription();
String getID();
String getName();
Методы явно делятся на 2 вида: методы, производящие действия с уже данными параметрами и методы, дающие информацию об этом бандлере.
execute принимает Map параметров и выходную директорию в качестве параметров и создаёт в данной директории бандл в каком-то ожидаемом формате. Но ожидать правильного исполнения мы можем только если validate проходит без исключений, а их он может кинуть 2 вида: UnsupportedPlatformException и ConfigException, говорящие названия которых подсказывают, что в случае выкидывания первого вы просто используете бандлер, который не поддерживается вашей платформой. Второй же выкидывается, если у вас что-то не так с переданными параметрами.
А что вообще можно передавать в качестве параметров в эти методы? Большая часть ключей в этих методах находится во всяких статических экземплярах класса BundlerParamInfo, большая часть из которых (общие кроссплатформенные параметры) находятся в классе StandardBundlerParam, а более специфичные (платформенно-зависимые), как правило, находятся в самих классах-реализациях Bundler'а: LinuxAppBundler, WinExeBundler и так далее. Хотя не совсем очевидно, каким образом этот класс вообще связан с тем, что мы кладём в params, который передаём execute. На самом деле оказывается, что id, получаемый getID(), является ключом в этом Map'е, а значение должно быть типа, которым параметризован BundlerParamInfo.
А как получить экземпляры бандлеров?
Самый простой метод получения всех возможных «предустановленных» Bundler'ов через статический метод createBundlersInstance интерфейса Bundlers (Warning, HOT: Java 8):
public static List<Bundler> getSuitableBundlers() {
return Bundlers.createBundlersInstance()
.getBundlers()
.stream()
.filter(bundler -> {
try {
bundler.validate(Collections.emptyMap());
} catch (UnsupportedPlatformException ex) {
return false;
} catch (ConfigException ignored) {
}
return true;
}).collect(Collectors.toList());
}
Таким образом, мы не только получили все «предустановленные» бандлеры, но и отфильтровали их по платформенному принципу: как правило, невозможно собирать пакеты для одной платформы на другой.
И что, никаких примеров?!
А, точно, можно попробовать перестать теоретизировать и нарисовать какой-нибудь пример.
Поскольку мне кажется, что статическая типизация всегда лучше, то мы можем обернуть создание ассоциативного массива параметров во что-то типа BunderParamsBuilder:
public class BundlerParamsBuilder {
private Map<String, Object> params = new HashMap<>();
public <T> BundlerParamsBuilder setParam(BundlerParamInfo<T> param, T value) {
params.put(param.getID(), value);
return this;
}
public BundlerParamsBuilder unsafeSetParam(String key, Object value) {
params.put(key, value);
return this;
}
public Map<String, Object> build() {
return new HashMap<>(params);
}
}
Таким образом, сборка проекта будет заключаться примерно в следующем (подразумевается существование запускающегося /tmp/helloWorld.jar):
List<Bundler> bundlers = getSuitableBundlers();
Path directoryWithBundles = Files.createTempDirectory("bundles");
Path jar = Paths.get("/tmp/helloWorld.jar");
RelativeFileSet mainJar = new RelativeFileSet(jar.getParent().toFile(), new HashSet<File>(
Arrays.asList(jar.toFile())
));
Map<String, Object> params = new BundlerParamsBuilder()
.setParam(StandardBundlerParam.APP_NAME, "HelloWorld")
.setParam(StandardBundlerParam.APP_RESOURCES, mainJar)
.build();
bundlers.forEach(bundler -> bundler.execute(params, directoryWithBundles.toFile()));
System.out.println("Bundles are created in " + directoryWithBundles);
System.out.println("Parameters after bundling: " + params);
Выхлоп после запуска будет примерно следующий:
Bundles are created in /tmp/bundles5791581710818077755 Parameters after bundling: {appVersion=1.0, copyright=Copyright (C) 2015, stopOnUninstall=true, .mac-jdk.runtime.rules=[Lcom.oracle.tools.packager.JreUtils$Rule;@4c3e4790, mac.app.bundler=Mac Application Image, linux.deb.imageRoot=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0/opt, buildRoot=/tmp/fxbundler6592356981290936843, mac.bundle-id-signing-prefix=HelloWorld., linux.deb.licenseText=Unknown, linux.deb.maintainer=Unknown <Unknown>, jvmProperties={}, mac.signing-key-user-name=, licenseFile=[], identifier=HelloWorld, linux.rpm.imageDir=/tmp/fxbundler6592356981290936843/images/linux-rpm.image, runtime=RelativeFileSet{basedir:/home/dginzburg/soft/jdk1.8.0_25/jre, files:[<very big list of files>]}, shortcutHint=false, mainJar=RelativeFileSet{basedir:/tmp, files:[helloWorld.jar]}, jvmOptions=[], name.fs=HelloWorld, fxPackaging=false, name=HelloWorld, appResources=RelativeFileSet{basedir:/tmp, files:[helloWorld.jar]}, mac.category=Unknown, linux.deb.imageDir=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0, .mac.default.icns=GenericAppHiDPI.icns, runAtStartup=false, linux.app.bundler=Linux Application Image, mac.signing-key-developer-id-app=null, linux.launcher.url=jar:file:/home/dginzburg/soft/jdk1.8.0_25/lib/ant-javafx.jar!/com/oracle/tools/packager/linux/JavaAppLauncher, description=HelloWorld, configRoot=/tmp/fxbundler6592356981290936843/macosx, preferencesID=HelloWorld, title=HelloWorld, linux.bundleName=helloworld, startOnInstall=false, mac.pkg.packagesRoot=/tmp/fxbundler6592356981290936843/packages, licenseType=Unknown, linux.deb.fullPackageName=helloworld-1.0, mac.CFBundleIdentifier=HelloWorld, serviceHint=false, vendor=Unknown, email=Unknown, applicationCategory=Unknown, mac.app.imageRoot=/tmp/fxbundler6592356981290936843/images/dmg.image, userJvmOptions={}, classpath=, linux.deb.configDir=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0/DEBIAN, verbose=false, imagesRoot=/tmp/fxbundler6592356981290936843/images, mac.daemon.image=/tmp/fxbundler6592356981290936843/images/pkg.daemon, applicationClass=HelloWorld, .linux.runtime.rules=[Lcom.oracle.tools.packager.JreUtils$Rule;@38cccef, menuHint=true}
Ой, откуда у нас всё это тут взялось? Мы вроде клали в params только 2 параметра. Чтобы разобраться в этом, надо посмотреть на сорцы метода BunderParamInfo.fetchFrom, который используется для того, чтобы получить значение параметра из params:
//...
if (getDefaultValueFunction() != null) {
T result = getDefaultValueFunction().apply(params);
if (result != null) {
params.put(getID(), result);
}
return result;
}
//...
Ага, если параметр не был найден в params и он может быть получен через другие параметры (за это отвечает функция defaultValueFunction), то значение, полученное таким образом, запихивается в params. Например, мы не указали параметр MAIN_JAR, но MAIN_JAR обязателен для того, чтобы создать запускаемый файл. Посмотрим на то, как определяется MAIN_JAR: в качестве defaultValueFunction видим:
params -> {
extractMainClassInfoFromAppResources(params);
return (RelativeFileSet) params.get("mainJar");
}
Вот и ответ, откуда там появляется значение mainJar.
Полный список параметров
Для того, чтобы не искать каждый раз параметры, я сделал табличку, в которую вывел информацию по всем найденным статическим экземплярам BundlerParamInfo.
Заключение
Описан самый минимум и самые очевидные возможности, так что не стесняйтесь читать JavaDoc и даже код.
Статья будет улучшаться и исправляться по запросу комментаторов.