/******************************************************************************* * Copyright 2014 Rafael Garcia Moreno. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.bladecoder.engineeditor.ui; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import org.apache.commons.io.FileUtils; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.ui.SelectBox; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.scenes.scene2d.ui.TextField; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.Timer.Task; import com.badlogicgames.packr.Packr; import com.badlogicgames.packr.PackrConfig; import com.badlogicgames.packr.PackrConfig.Platform; import com.bladecoder.engine.actions.Param.Type; import com.bladecoder.engine.util.Config; import com.bladecoder.engineeditor.Ctx; import com.bladecoder.engineeditor.common.Message; import com.bladecoder.engineeditor.common.RunProccess; import com.bladecoder.engineeditor.ui.panels.EditDialog; import com.bladecoder.engineeditor.ui.panels.FileInputPanel; import com.bladecoder.engineeditor.ui.panels.InputPanel; import com.bladecoder.engineeditor.ui.panels.InputPanelFactory; public class PackageDialog extends EditDialog { private static final String ARCH_PROP = "package.arch"; private static final String DIR_PROP = "package.dir"; private static final String DESKTOP_LAUNCHER = "DesktopLauncher.java"; private static final String INFO = "Package the Adventure for distribution"; // private static final String[] ARCHS = { "desktop", "android", "ios", "html" }; private static final String[] ARCHS = { "desktop", "android", "ios" }; private static final String[] TYPES = { "Bundle JRE", "Runnable jar" }; private static final String[] OSS = { "all", "windows", "linux64", "linux32", "macOSX" }; private InputPanel arch; private InputPanel dir; private InputPanel type; private InputPanel os; private InputPanel linux64JRE; private InputPanel linux32JRE; private InputPanel winJRE; private InputPanel osxJRE; private InputPanel version; private InputPanel icon; private InputPanel versionCode; private InputPanel androidSDK; private InputPanel androidKeyStore; private InputPanel androidKeyAlias; private InputPanel androidKeyStorePassword; private InputPanel androidKeyAliasPassword; private InputPanel iosSignIdentity; private InputPanel iosProvisioningProfile; private InputPanel[] options = new InputPanel[14]; @SuppressWarnings("unchecked") public PackageDialog(Skin skin) { super("PACKAGE ADVENTURE", skin); arch = InputPanelFactory.createInputPanel(skin, "Architecture", "Select the target Architecture for the game", ARCHS, true); dir = new FileInputPanel(skin, "Output Directory", "Select the output directory to put the package", FileInputPanel.DialogType.DIRECTORY); type = InputPanelFactory.createInputPanel(skin, "Type", "Select the package type", TYPES, true); os = InputPanelFactory.createInputPanel(skin, "OS", "Select the OS of the package", OSS, true); linux64JRE = new FileInputPanel(skin, "JRE.Linux64", "Select the 64 bits Linux JRE Location to bundle. Must be a ZIP file", FileInputPanel.DialogType.OPEN_FILE); linux32JRE = new FileInputPanel(skin, "JRE.Linux32", "Select the 32 bits Linux JRE Location to bundle. Must be a ZIP file", FileInputPanel.DialogType.OPEN_FILE); winJRE = new FileInputPanel(skin, "JRE.Windows64", "Select the Windows 64 bits JRE Location to bundle. Must be a ZIP file", FileInputPanel.DialogType.OPEN_FILE); osxJRE = new FileInputPanel(skin, "JRE.MACOS", "Select the MacOS JRE Location to bundle. Must be a ZIP file", FileInputPanel.DialogType.OPEN_FILE); version = InputPanelFactory.createInputPanel(skin, "Version", "Select the package version", true); icon = new FileInputPanel(skin, "MacOS Icon", "The icon (.icns) for the Mac package. It is not mandatory.", FileInputPanel.DialogType.OPEN_FILE, false); versionCode = InputPanelFactory.createInputPanel(skin, "Version Code", "An integer that identify the version.", Type.INTEGER, true); androidSDK = new FileInputPanel(skin, "SDK", "Select the Android SDK Location. If empty, the ANDROID_HOME variable will be used.", FileInputPanel.DialogType.DIRECTORY, false); androidKeyStore = new FileInputPanel(skin, "KeyStore", "Select the Key Store Location", FileInputPanel.DialogType.OPEN_FILE); androidKeyAlias = InputPanelFactory.createInputPanel(skin, "KeyAlias", "Select the Key Alias Location", true); androidKeyStorePassword = InputPanelFactory .createInputPanel(skin, "KeyStorePasswd", "Key Store Password", true); androidKeyAliasPassword = InputPanelFactory .createInputPanel(skin, "KeyAliasPasswd", "Key Alias Password", true); iosSignIdentity = InputPanelFactory .createInputPanel(skin, "Sign Identity", "Empty for auto select.\nThis field matches against the start of the certificate name. Alternatively you can use a certificate fingerprint.\nIf the value is enclosed in / a regexp search will be done against the certificate name instead.\nRun the command 'security find-identity -v -p codesigning' or use theKeyChain Access OS X app to view your installed certificates.", false); iosProvisioningProfile = InputPanelFactory .createInputPanel(skin, "Provisioning Profile", "Empty for auto select.", false); options[0] = type; options[1] = os; options[2] = linux64JRE; options[3] = linux32JRE; options[4] = winJRE; options[5] = osxJRE; options[6] = version; options[7] = icon; options[8] = versionCode; options[9] = androidSDK; options[10] = androidKeyStore; options[11] = androidKeyAlias; options[12] = iosSignIdentity; options[13] = iosProvisioningProfile; addInputPanel(arch); addInputPanel(dir); for (InputPanel i : options) { addInputPanel(i); } addInputPanel(androidKeyStorePassword); addInputPanel(androidKeyAliasPassword); ((TextField) androidKeyStorePassword.getField()).setPasswordMode(true); ((TextField) androidKeyStorePassword.getField()).setPasswordCharacter('*'); ((TextField) androidKeyAliasPassword.getField()).setPasswordMode(true); ((TextField) androidKeyAliasPassword.getField()).setPasswordCharacter('*'); dir.setMandatory(true); arch.setText(Ctx.project.getEditorConfig().getProperty(ARCH_PROP, ARCHS[0])); dir.setText(Ctx.project.getEditorConfig().getProperty(DIR_PROP, "")); for (InputPanel i : options) { String prop = Ctx.project.getEditorConfig().getProperty("package." + i.getTitle()); if (prop != null && !prop.isEmpty()) i.setText(prop); } version.setText(getCurrentVersion()); // TODO Set version code based in version // androidVersionCode.setText(genVersionCode(version.getText())); setInfo(INFO); ((SelectBox<String>) (arch.getField())).addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { archChanged(); } }); ((SelectBox<String>) (type.getField())).addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { typeChanged(); } }); ((SelectBox<String>) (os.getField())).addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { osChanged(); } }); archChanged(); } @Override protected void ok() { Message.showMsg(getStage(), "Generating package...", true); new Thread() { Stage stage = getStage(); @Override public void run() { String msg; if (Ctx.project.getSelectedScene() == null) { msg = "There are no scenes in this chapter."; Message.showMsg(getStage(), msg, 3); return; } Ctx.project.getProjectConfig().remove(Config.CHAPTER_PROP); Ctx.project.getProjectConfig().remove(Config.TEST_SCENE_PROP); setCurrentVersion(version.getText()); try { Ctx.project.saveProject(); } catch (Exception ex) { msg = "Something went wrong while saving the project.\n\n" + ex.getClass().getSimpleName() + " - " + ex.getMessage(); Message.showMsgDialog(getStage(), "Error", msg); return; } try { msg = packageAdv(); } catch (Exception e) { msg = "Error Generating package\n\n" + e.getMessage(); e.printStackTrace(); } Ctx.project.getEditorConfig().setProperty(ARCH_PROP, arch.getText()); Ctx.project.getEditorConfig().setProperty(DIR_PROP, dir.getText()); for (InputPanel i : options) { if (i.getText() != null) Ctx.project.getEditorConfig().setProperty("package." + i.getTitle(), i.getText()); } Message.hideMsg(); if (msg != null) { final String m = msg; Timer.post( new Task() { @Override public void run() { Message.showMsgDialog(stage, "Result", m); } }); } } }.start(); } private String packageAdv() throws IOException { String msg = "Package generated SUCCESSFULLY"; String projectName = getAppName(); if (projectName == null) Ctx.project.getProjectDir().getName(); String versionParam = "-Pversion=" + version.getText() + " "; Ctx.project.getProjectConfig().setProperty(Config.VERSION_PROP, version.getText()); if (arch.getText().equals("desktop")) { String jarDir = Ctx.project.getProjectDir().getAbsolutePath() + "/desktop/build/libs/"; String jarName = projectName + "-desktop-" + version.getText() + ".jar"; String error = genDesktopJar(projectName, versionParam, jarDir, jarName); if(error != null) msg = error; if (type.getText().equals(TYPES[0])) { // BUNDLE JRE String launcher = getDesktopMainClass(); if (os.getText().equals("linux64")) { packr(Platform.Linux64, linux64JRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); } else if (os.getText().equals("linux32")) { packr(Platform.Linux32, linux32JRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); } else if (os.getText().equals("windows")) { packr(Platform.Windows64, winJRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); } else if (os.getText().equals("macOSX")) { packr(Platform.MacOS, osxJRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); } else if (os.getText().equals("all")) { packr(Platform.Linux64, linux64JRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); packr(Platform.Linux32, linux32JRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); packr(Platform.Windows64, winJRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); packr(Platform.MacOS, osxJRE.getText(), projectName, jarDir + jarName, launcher, dir.getText()); } } } else if (arch.getText().equals("android")) { String params = versionParam + "-PversionCode=" + versionCode.getText() + " " + "-Pkeystore=" + androidKeyStore.getText() + " " + "-PstorePassword=" + androidKeyStorePassword.getText() + " " + "-Palias=" + androidKeyAlias.getText() + " " + "-PkeyPassword=" + androidKeyAliasPassword.getText() + " "; // UPDATE 'local.properties' with the android SDK location. if (androidSDK.getText() != null && !androidSDK.getText().trim().isEmpty()) { String sdk = androidSDK.getText(); Properties p = new Properties(); p.setProperty("sdk.dir", sdk); p.store(new FileOutputStream( new File(Ctx.project.getProjectDir().getAbsolutePath(), "local.properties")), null); } if (RunProccess.runGradle(Ctx.project.getProjectDir(), params + "android:assembleRelease")) { String apk = Ctx.project.getProjectDir().getAbsolutePath() + "/android/build/outputs/apk/android-release.apk"; File f = new File(apk); // FileUtils.copyFileToDirectory(f, new File(dir.getText())); FileUtils.copyFile(f, new File(dir.getText(), projectName + "-" + version.getText() + ".apk")); } else { msg = "Error Generating package"; } } else if (arch.getText().equals("ios")) { if (!System.getProperty("os.name").toLowerCase().contains("mac")) { return "You need a MacOSX computer with XCode installed to generate the IOS package."; } // UPDATE 'robovm.properties' Properties p = new Properties(); p.load(new FileReader(Ctx.project.getProjectDir().getAbsolutePath() + "/ios/robovm.properties")); p.setProperty("app.version", version.getText()); p.setProperty("app.build", versionCode.getText()); p.setProperty("app.name", projectName); p.store(new FileOutputStream( new File(Ctx.project.getProjectDir().getAbsolutePath(), "/ios/robovm.properties")), null); List<String> params = new ArrayList<String>(); if(iosSignIdentity.getText() != null) params.add("-Probovm.iosSignIdentity=" + iosSignIdentity.getText()); if(iosProvisioningProfile.getText() != null) params.add("-Probovm.iosProvisioningProfile=" + iosProvisioningProfile.getText() ); // Add clean target in IOS because the app. is not signing well if not cleaning. params.add("ios:clean"); params.add("ios:createIPA"); if (RunProccess.runGradle(Ctx.project.getProjectDir(), params)) { String apk = Ctx.project.getProjectDir().getAbsolutePath() + "/ios/build/robovm/IOSLauncher.ipa"; File f = new File(apk); FileUtils.copyFile(f, new File(dir.getText(), projectName + "-" + version.getText() + ".ipa")); } else { msg = "Error Generating package"; } } else if (arch.getText().equals("html")) { if (RunProccess.runGradle(Ctx.project.getProjectDir(), "html:dist")) { FileUtils.copyDirectory(new File(Ctx.project.getProjectDir().getAbsolutePath() + "/html/build/dist"), new File(dir.getText())); } else { msg = "Error Generating package"; } } return msg; } private void archChanged() { for (InputPanel ip : options) { setVisible(ip, false); } setVisible(androidKeyStorePassword, false); setVisible(androidKeyAliasPassword, false); setVisible(version, true); String a = arch.getText(); if (a.equals("desktop")) { setVisible(type, true); typeChanged(); } else if (a.equals("android")) { setVisible(versionCode, true); setVisible(androidSDK, true); setVisible(androidKeyStore, true); setVisible(androidKeyAlias, true); setVisible(androidKeyStorePassword, true); setVisible(androidKeyAliasPassword, true); } else if (a.equals("ios")) { setVisible(versionCode, true); setVisible(iosSignIdentity, true); setVisible(iosProvisioningProfile, true); } else if (a.equals("html")) { Message.showMsgDialog(getStage(), "Not Supported", "HTML export is not supported yet."); } } private void typeChanged() { if (type.getText().equals(TYPES[0])) { setVisible(os, true); } else { setVisible(os, false); setVisible(icon, false); } osChanged(); } private void osChanged() { setVisible(icon, false); if (os.isVisible() && (os.getText().equals("windows") || os.getText().equals("all"))) { setVisible(winJRE, true); } else { setVisible(icon, false); setVisible(winJRE, false); } if (os.isVisible() && (os.getText().equals("linux32") || os.getText().equals("all"))) { setVisible(linux32JRE, true); } else { setVisible(linux32JRE, false); } if (os.isVisible() && (os.getText().equals("linux64") || os.getText().equals("all"))) { setVisible(linux64JRE, true); } else { setVisible(linux64JRE, false); } if (os.isVisible() && (os.getText().equals("macOSX") || os.getText().equals("all"))) { setVisible(osxJRE, true); setVisible(icon, true); } else { setVisible(osxJRE, false); } } @Override protected boolean validateFields() { boolean ok = true; if (!dir.validateField()) ok = false; for (InputPanel i : options) { if (i.isVisible() && !i.validateField()) ok = false; } if (androidKeyStorePassword.isVisible() && !androidKeyStorePassword.validateField()) ok = false; if (androidKeyAliasPassword.isVisible() && !androidKeyAliasPassword.validateField()) ok = false; // if (icon.isVisible() && !icon.getText().endsWith(".ico")) { // icon.setError(true); // ok = false; // } if (linux32JRE.isVisible() && (!new File(linux32JRE.getText()).exists() || !linux32JRE.getText().toLowerCase().endsWith(".zip"))) { linux32JRE.setError(true); ok = false; } if (linux64JRE.isVisible() && (!new File(linux64JRE.getText()).exists() || !linux64JRE.getText().toLowerCase().endsWith(".zip"))) { linux64JRE.setError(true); ok = false; } if (winJRE.isVisible() && (!new File(winJRE.getText()).exists() || !winJRE.getText().toLowerCase().endsWith(".zip"))) { winJRE.setError(true); ok = false; } if (osxJRE.isVisible() && (!new File(osxJRE.getText()).exists() || !osxJRE.getText().toLowerCase().endsWith(".zip"))) { osxJRE.setError(true); ok = false; } return ok; } private String genDesktopJar(String projectName, String versionParam, String jarDir, String jarName) throws IOException { String msg = null; if (RunProccess.runGradle(Ctx.project.getProjectDir(), versionParam + "desktop:dist")) { File f = new File(jarDir + jarName); FileUtils.copyFileToDirectory(f, new File(dir.getText())); new File(jarDir, jarName).setExecutable(true); new File(dir.getText(), jarName).setExecutable(true); } else { msg = "Error Generating package"; } return msg; } private void packr(Platform platform, String jdk, String exe, String jar, String mainClass, String outDir) throws IOException { String suffix = null; switch (platform) { case Linux32: suffix = "lin32"; break; case Linux64: suffix = "lin64"; break; case MacOS: suffix = "mac.app"; break; case Windows64: case Windows32: suffix = "win"; break; } PackrConfig config = new PackrConfig(); config.platform = platform; config.jdk = jdk; config.executable = exe; config.classpath = Arrays.asList(jar); config.mainClass = mainClass.replace('/', '.'); config.vmArgs = Arrays.asList("-Xmx1G"); config.minimizeJre = "hard"; config.outDir = new File(outDir + "/" + exe + "-" + suffix); new Packr().pack(config); // COPY MAC OS ICON if(platform == Platform.MacOS && icon.getText() != null && icon.getText().endsWith(".icns")) { FileUtils.copyFile(new File(icon.getText()), new File(config.outDir.getAbsolutePath() + "/Contents/Resources/icons.icns")); } } /** * @return The appName from the file gradle.properties from the game */ private String getAppName() { try { Properties prop = Ctx.project.getGradleProperties(); return prop.getProperty("appName"); } catch (IOException e) { Message.showMsg(getStage(), "Error reading file 'gradle.properties' from the game.", 3); } return null; } /** * @return The version from the file gradle.properties from the game */ private String getCurrentVersion() { try { Properties prop = Ctx.project.getGradleProperties(); return prop.getProperty("version"); } catch (IOException e) { Message.showMsg(getStage(), "Error reading file 'gradle.properties' from the game.", 3); } return null; } /** * Saves the selected version */ private void setCurrentVersion(String version) { try { Properties prop = Ctx.project.getGradleProperties(); prop.setProperty("version", version); Ctx.project.saveGradleProperties(prop); } catch (IOException e) { Message.showMsg(getStage(), "Error reading file 'gradle.properties' from the game.", 3); } } /** * @return Search the desktop main class in the desktop folder */ private String getDesktopMainClass() { File result = search(new File(Ctx.project.getProjectDir().getAbsolutePath() + "/desktop")); String absolutePath = result.getAbsolutePath().replace('\\', '/'); int cutIdx = absolutePath.indexOf("src/") + 4; return absolutePath.substring(cutIdx, absolutePath.length() - 5); } private File search(File file) { File result = null; // do you have permission to read this directory? if (file.canRead()) { for (File temp : file.listFiles()) { if (temp.isDirectory()) { result = search(temp); if(result != null) return result; } else { if (temp.getName().equals(DESKTOP_LAUNCHER)) { return temp; } } } } return result; } }