All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.qt.qtjambi.deployer.AppGenerator Maven / Gradle / Ivy

There is a newer version: 6.8.1
Show newest version
/****************************************************************************
**
** Copyright (C) 2009-2022 Dr. Peter Droste, Omix Visualization GmbH & Co. KG. All rights reserved.
**
** This file is part of Qt Jambi.
**
** ** $BEGIN_LICENSE$
** GNU Lesser General Public License Usage
** This file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
** 
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
** $END_LICENSE$

**
** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
**
****************************************************************************/
package io.qt.qtjambi.deployer;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.AbstractMap.SimpleEntry;
import java.util.function.IntFunction;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import io.qt.core.QByteArray;
import io.qt.core.QCborArray;
import io.qt.core.QCborMap;
import io.qt.core.QCborValue;
import io.qt.core.QCommandLineOption;
import io.qt.core.QCommandLineParser;
import io.qt.core.QDir;
import io.qt.core.QFile;
import io.qt.core.QFileDevice;
import io.qt.core.QIODevice;
import io.qt.core.QList;
import io.qt.core.QStringList;
import io.qt.qtjambi.deployer.Main.JVMDetectionModes;
import io.qt.qtjambi.deployer.Main.Parameters;

class AppGenerator {
	static void generate(QCommandLineParser parser, String[] args, QCommandLineOption dirOption, QCommandLineOption classPathOption, QCommandLineOption configurationOption) throws InterruptedException, IOException{
	    QCommandLineOption applicationJNIMinVersionOption = new QCommandLineOption(QList.of("jni-minimum-version"), "Minimum version for the required JNI", "version");
	    QCommandLineOption applicationNameOption = new QCommandLineOption(QList.of("application", "application-name"), "Application name", "name");
	    QCommandLineOption applicationIcoOption = new QCommandLineOption(QList.of("ico", "application-icon"), "Application icon", "file");
	    QCommandLineOption applicationMPOption = new QCommandLineOption(QList.of("mp", "module-path"), "Module path for application execution", "path");
	    QCommandLineOption applicationLPOption = new QCommandLineOption(QList.of("lp", "library-path"), "Library path for application execution", "path");
	    QCommandLineOption applicationMainClassOption = new QCommandLineOption(QList.of("main-class"), "Main class", "class");
	    QCommandLineOption applicationAutodetectJvmOption = new QCommandLineOption(QList.of("autodetect-jvm"), "Autodetect Java Virtual Machine at runtime");
	    QCommandLineOption applicationJVMMinVersionOption = new QCommandLineOption(QList.of("minversion-jvm", "jvm-minimum-version"), "Minimum version for the autodetected Java Virtual Machine", "version");
	    QCommandLineOption applicationJVMPathOption = new QCommandLineOption(QList.of("jvm-path"), "Path to Java Virtual Machine (absolute or relative to app binary)", "version");
	    QCommandLineOption applicationExeOption = new QCommandLineOption(QList.of("executable"), "Path to executable file.\nExamples:\n--executable=path"+File.separator+"QtJambiLauncher.exe\n--executable=macos"+File.pathSeparator+"path"+File.separator+"QtJambiLauncher.app", "file");
	    QCommandLineOption applicationExeLocationOption = new QCommandLineOption(QList.of("executable-location"), "Directory containing QtJambiLauncher executable", "path");
	    QCommandLineOption applicationJVMArgOption = new QCommandLineOption(QList.of("jvmarg"), "JVM argument", "arg");
	    parser.addOptions(QList.of(
	    		configurationOption,
	    		
	    		applicationJNIMinVersionOption,
	    		applicationNameOption,
	    		applicationIcoOption,
	    		applicationMPOption,
	    		applicationLPOption,
	    		applicationMainClassOption,
	    		applicationAutodetectJvmOption,
	    		applicationJVMMinVersionOption,
	    		applicationJVMPathOption,
	    		applicationExeOption,
	    		applicationExeLocationOption,
	    		applicationJVMArgOption,
	    		
	    		dirOption,
	    		classPathOption
			));
	    parser.addPositionalArgument("jvmargs", "Argument for Java Virtual Machine");
	    
		if(args.length==1)
			parser.showHelp();
    	parser.process(new QStringList(args));
		
		QStringList additionalArguments = new QStringList(parser.positionalArguments());
		
		String appName = null;
		if(parser.isSet(applicationNameOption))
			appName = parser.value(applicationNameOption);
		
		String mainClass = null;
		if(parser.isSet(applicationMainClassOption))
			mainClass = parser.value(applicationMainClassOption);
		
		String jvmPath = null;
		if(parser.isSet(applicationJVMPathOption))
			jvmPath = parser.value(applicationJVMPathOption);

		QDir dir = null;
		if(parser.isSet(dirOption))
			dir = new QDir(QDir.fromNativeSeparators(parser.value(dirOption)));
		
		QFile ico = null;
		if(parser.isSet(applicationIcoOption))
			ico = new QFile(QDir.fromNativeSeparators(parser.value(applicationIcoOption)));
		
		Integer jniMinimumVersion = null;
		if(parser.isSet(applicationJNIMinVersionOption))
			jniMinimumVersion = Integer.parseInt(parser.value(applicationJNIMinVersionOption), 16);
		
		List classPaths = new ArrayList<>();
		if(parser.isSet(classPathOption))
			classPaths.addAll(Arrays.asList(parser.value(classPathOption).split(File.pathSeparator)));
		
		List modulePaths = new ArrayList<>();
		if(parser.isSet(applicationMPOption))
			modulePaths.addAll(Arrays.asList(parser.value(applicationMPOption).split(File.pathSeparator)));
		
		List libraryPaths = new ArrayList<>();
		if(parser.isSet(applicationLPOption))
			libraryPaths.addAll(Arrays.asList(parser.value(applicationLPOption).split(File.pathSeparator)));

		List> executables = new ArrayList<>();
		if(parser.isSet(applicationExeOption)) {
			String[] exeinfo = parser.value(applicationExeOption).split(File.pathSeparator);
			if(exeinfo.length==2){
				File exeFile = new File(exeinfo[1]);
				if(!exeFile.isFile()) {
					throw new Error("Specified launcher executable does not exist: "+exeinfo[1]);
				}
				executables.add(new SimpleEntry<>(exeinfo[0], exeFile.toURI().toURL()));
			}else {
				String os = null;
				File exeFile = new File(exeinfo[0]);
				if(exeinfo[0].endsWith(".exe")) {
					os = "windows";
					if(!exeFile.isFile()) {
						throw new Error("Specified launcher executable does not exist: "+exeinfo[0]);
					}
				}else if(exeinfo[0].endsWith(".app")) {
					if(!exeFile.isDirectory()) {
						throw new Error("Specified launcher executable does not exist: "+exeinfo[0]);
					}
					os = "macos";
				}else {
					if(!exeFile.isFile()) {
						throw new Error("Specified launcher executable does not exist: "+exeinfo[0]);
					}
					os = "linux";
				}
				if(os!=null) {
					executables.add(new SimpleEntry<>(os, exeFile.toURI().toURL()));
				}
			}
		}else if(parser.isSet(applicationExeLocationOption)) {
			QDir location = new QDir(parser.value(applicationExeLocationOption));
			for(String entry : location.entryList(QDir.Filter.Files)) {
                String os = null;
                if(entry.equals("QtJambiLauncher.exe")) {
                    os = "windows";
                }else if(entry.equals("QtJambiLauncher.app")) {
                    os = "macos";
                }else if(entry.equals("QtJambiLauncher")) {
                    os = "linux";
                }
                if(os!=null && entry.contains("QtJambiLauncher")) {
                	File libFile = new File(location.absoluteFilePath(entry));
                	executables.add(new SimpleEntry<>(os, libFile.toURI().toURL()));
                }
			}
		}
		
        boolean autoDetect = parser.isSet(applicationAutodetectJvmOption);
        
        int minimumJVMVersion = 11;
		if(parser.isSet(applicationJVMMinVersionOption))
			minimumJVMVersion = Integer.parseInt(parser.value(applicationJVMMinVersionOption));
		
		List arguments = new ArrayList<>(additionalArguments);
		if(parser.isSet(applicationJVMArgOption))
			arguments.addAll(parser.values(applicationJVMArgOption));

		if(appName==null || appName.isEmpty()) {
			throw new Error("Missing application name. Please use --application=...");
		}
		if(!autoDetect && (jvmPath==null || jvmPath.isEmpty())) {
			throw new Error("Missing JVM path. Please use --jvm-path=...");
		}
		if(dir==null) {
			throw new Error("Missing target directory. Please use --dir=...");
		}
		if(mainClass==null || mainClass.isEmpty()) {
			throw new Error("Missing main class. Please use --main-class=...");
		}
		if(executables.isEmpty()) {
			Enumeration specsFound = Main.findSpecs();
            Set specsUrls = new HashSet<>();
            while (specsFound.hasMoreElements()) {
                URL url = specsFound.nextElement();
                if(specsUrls.contains(url))
                	continue;
                specsUrls.add(url);
                String protocol = url.getProtocol();
                if (protocol.equals("jar")) {
                	String eform = url.toExternalForm();
                	int start = 4;
                	int end = eform.indexOf("!/", start);
                	if (end != -1) {
                		try {
	                        Document doc;
	                        try(InputStream inStream = url.openStream()){
	                        	DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	                        	factory.setValidating(false);
	                			DocumentBuilder builder = factory.newDocumentBuilder();
	                			doc = builder.parse(inStream);
	                        }
	                        String system = doc.getDocumentElement().getAttribute("system");
//                        String configuration = doc.getDocumentElement().getAttribute("configuration");
	                        for(int i=0; i(system, libraryURL));
	                        		}
	                        	}
	                        }
                		}catch (Exception e) {
            				Logger.getLogger("io.qt").log(Level.WARNING, "", e);
            			}
                	}
                }
            }
		}
		if(executables.isEmpty()) {
			throw new Error("Missing paths to QtJambiLauncher executables. Please use --executable=, --executable="+File.pathSeparatorChar+" or --executable-location=");
		}
		//System.out.println("Collecting metadata...");
		//System.out.flush();
		QCborMap cborValue = new QCborMap();
		cborValue.setValue(Parameters.JVMDetectionMode.value(), new QCborValue(autoDetect ? JVMDetectionModes.AutoDetect.value() : JVMDetectionModes.UseRelativePath.value()));
		if(autoDetect) {
			cborValue.setValue(Parameters.MinimumJVM.value(), new QCborValue(minimumJVMVersion));
		}else {
			cborValue.setValue(Parameters.JVMPath.value(), new QCborValue(jvmPath));				
		}
		IntFunction arrayFactory = length->new QCborValue[length];
		cborValue.setValue(Parameters.MainClass.value(), new QCborValue(mainClass));
		QCborArray argumentArray = new QCborArray(arguments.stream().map(QByteArray::new).map(QCborValue::new).toArray(arrayFactory));
		cborValue.setValue(Parameters.JVMArguments.value(), new QCborValue(argumentArray));
		QCborArray classPathArray = new QCborArray(classPaths.stream().map(QCborValue::new).toArray(arrayFactory));
		cborValue.setValue(Parameters.ClassPath.value(), new QCborValue(classPathArray));
		QCborArray modulePathArray = new QCborArray(modulePaths.stream().map(QCborValue::new).toArray(arrayFactory));
		cborValue.setValue(Parameters.ModulePath.value(), new QCborValue(modulePathArray));
		QCborArray libraryPathArray = new QCborArray(libraryPaths.stream().map(QCborValue::new).toArray(arrayFactory));
		cborValue.setValue(Parameters.LibraryPath.value(), new QCborValue(libraryPathArray));
		if(jniMinimumVersion!=null) {
			cborValue.setValue(Parameters.JNIMinimumVersion.value(), new QCborValue(jniMinimumVersion));
		}
		QByteArray cborData = cborValue.toCborValue().toCbor();
		if(cborData.size()>16384) {
			throw new Error("Launcher metadata exceeds maximum size of 16384 byte.");
		}
		System.gc();
		final QByteArray QTJAMBI_LAUNCHER = new QByteArray("QTJAMBI_LAUNCHER!");
		
		for(Map.Entry entry : executables) {
			String os = entry.getKey();
			if(os!=null) {
                URL file = entry.getValue();
				QFile newFile;
                switch(os.toLowerCase()) {
				case "linux":
				case "linux32":
				case "linux64":
				case "linux-arm64":
				case "linux-aarch64":
				case "linux-x86":
				case "linux-x64":
					newFile = new QFile(dir.absoluteFilePath(appName));
					break;
//				default: continue;
				case "win32":
				case "win64":
				case "windows":
				case "windows-aarch64":
				case "windows-arm64":
				case "windows-x86":
				case "windows-x64":
					newFile = new QFile(dir.absoluteFilePath(appName + ".exe"));
					System.gc();
					break;
				case "macos":
				case "osx":
					String fileBase = file.toExternalForm();
					if(fileBase.endsWith("/"))
						fileBase = fileBase.substring(0, fileBase.length()-1);
					String baseName = fileBase;
					{
						int idx = baseName.lastIndexOf('/');
						if(idx>0)
							baseName = baseName.substring(idx+1);
					}
					if(baseName.endsWith(".app"))
						baseName = baseName.substring(0, baseName.length()-4);
					dir.mkpath(appName + ".app/Contents/MacOS");
					dir.mkpath(appName + ".app/Contents/Resources");
					try {
						URL infoPListUrl = new URL(fileBase + "/Contents/Info.plist");
						QIODevice device = QIODevice.fromInputStream(infoPListUrl.openStream());
                        QByteArray data = device.readAll();
                        device.close();
                        int idx = (int)data.indexOf("CFBundleIdentifier");
						if(idx>0) {
							idx = (int)data.indexOf("", idx);
							if(idx>0) {
								idx += 8;
								int idxEnd = (int)data.indexOf("", idx);
								if(idxEnd>idx) {
									data = data.remove(idx, idxEnd-idx);
									data.insert(idx, mainClass);
								}
							}
						}
						data = data.replace(baseName, appName);
						data = data.replace("QtJambiLauncher", appName);
						QFile infoPList = new QFile(dir.absoluteFilePath(appName + ".app/Contents/Info.plist"));
						infoPList.remove();
						if(infoPList.open(QIODevice.OpenModeFlag.WriteOnly)) {
							infoPList.write(data);
							infoPList.close();
						}
					}catch(IOException e) {
						e.printStackTrace();
					}
                    try {
                    	URL pkgInfoUrl = new URL(fileBase + "/Contents/PkgInfo");
						QIODevice device = QIODevice.fromInputStream(pkgInfoUrl.openStream());
                        QByteArray data = device.readAll();
                        device.close();
                        dir.mkpath(appName + ".app/Contents");
                        QFile pkgInfo = new QFile(dir.absoluteFilePath(appName + ".app/Contents/PkgInfo"));
						pkgInfo.remove();
						if(pkgInfo.open(QIODevice.OpenModeFlag.WriteOnly)) {
							pkgInfo.write(data);
							pkgInfo.close();
						}
					}catch(IOException e) {
						e.printStackTrace();
					}
                    try {
	                    URL emptylprojUrl = new URL(fileBase + "/Contents/Resources/empty.lproj");
	                    QIODevice device = QIODevice.fromInputStream(emptylprojUrl.openStream());
                        QByteArray data = device.readAll();
                        device.close();
                        dir.mkpath(appName + ".app/Contents/Resources");
                        QFile emptylproj = new QFile(dir.absoluteFilePath(appName + ".app/Contents/Resources/empty.lproj"));
						emptylproj.remove();
						if(emptylproj.open(QIODevice.OpenModeFlag.WriteOnly)) {
							emptylproj.write(data);
							emptylproj.close();
						}
					}catch(IOException e) {
						e.printStackTrace();
					}
                    try {
	                    URL executableUrl = new URL(fileBase + "/Contents/MacOS/"+baseName);
	                    QIODevice device = QIODevice.fromInputStream(executableUrl.openStream());
                        QByteArray data = device.readAll();
                        device.close();
                        dir.mkpath(appName + ".app/Contents/MacOS");
                        QFile executable = new QFile(dir.absoluteFilePath(appName + ".app/Contents/MacOS/"+appName));
						executable.remove();
						if(executable.open(QIODevice.OpenModeFlag.WriteOnly)) {
							executable.write(data);
							executable.close();
							executable.setPermissions(executable.permissions()
						                                		.combined(QFileDevice.Permission.ExeGroup)
						                                		.combined(QFileDevice.Permission.ExeOther)
						                                		.combined(QFileDevice.Permission.ExeOwner)
						                                		.combined(QFileDevice.Permission.ExeUser));
						}
					}catch(IOException e) {
						e.printStackTrace();
					}
                    
                    dir.mkpath(appName + ".app/Contents/Resources");
                    QFile params = new QFile(dir.absoluteFilePath(appName + ".app/Contents/Resources/params.cbor"));
					params.remove();
					if(params.open(QIODevice.OpenModeFlag.WriteOnly)) {
						params.write(cborData);
						params.close();
					}
                    continue;
                    default: continue;
				}
                try {
                    QIODevice device = QIODevice.fromInputStream(file.openStream());
                    QByteArray exeData = device.readAll();
                    device.close();
                    int idx = (int)exeData.indexOf(QTJAMBI_LAUNCHER);
                    if(idx>0) {
                    	newFile.remove();
                        if(newFile.open(QIODevice.OpenModeFlag.WriteOnly)) {
                            try{
                                newFile.write(exeData);
                                while(idx>0) {
	                                newFile.seek((int)idx);
	                                newFile.write(cborData);
	                                idx = (int)exeData.indexOf(QTJAMBI_LAUNCHER, idx + cborData.length());
                                }
                                switch(os.toLowerCase()) {
        						case "win32":
        						case "win64":
        						case "windows":
                                	idx = (int)exeData.indexOf(new QByteArray("QTJAMBI_ICO!"));
                                	if(idx>254) {
                                        if(ico!=null) {
        									if(ico.open(QIODevice.OpenModeFlag.ReadOnly)) {
        										QByteArray icoData = ico.readAll();
        										if(icoData.size()>=92482) {
        			                                throw new Error("Icon size too big.");
        										}
        	                                    newFile.seek(idx-254);
        	                                    newFile.write(icoData);
        									}
                                        }else {
    	        							try(InputStream stream = Main.class.getResourceAsStream("icon.ico");
    	        									ByteArrayOutputStream buffer = new ByteArrayOutputStream()){
    	        								byte[] data = new byte[10240];
    	        								int length = stream.read(data);
    	        								while(length>0) {
    	        									buffer.write(data, 0, length);
    	        									length = stream.read(data);
    	        								}
    	        								data = buffer.toByteArray();
    	        								if(data.length<92482) {
	    	        								newFile.seek(idx-254);
	        	                                    newFile.write(data);
    	        								}
    	        							}
                                        }
                                	}
        							break;
        						default: 
        							break;
                                }
                            }finally {
                            	newFile.close();
                            }
                            newFile.setPermissions(newFile.permissions()
					                                		.combined(QFileDevice.Permission.ExeGroup)
					                                		.combined(QFileDevice.Permission.ExeOther)
					                                		.combined(QFileDevice.Permission.ExeOwner)
					                                		.combined(QFileDevice.Permission.ExeUser));
                        }else{
                            throw new Error("Unable to write file "+newFile.fileName());
                        }
                    }else {
                        throw new Error("Unable to find \""+QTJAMBI_LAUNCHER+"\" in file "+file.toExternalForm());
                    }
				}catch(IOException e) {
                    throw new Error("Unable to read file "+file.toExternalForm(), e);
				}
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy