/*
* Copyright (C) 2012 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
*/
package org.robovm.compiler.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.robovm.compiler.config.Arch;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.OS;
import org.robovm.compiler.config.tools.TextureAtlas;
import org.robovm.compiler.log.ConsoleLogger;
import org.robovm.compiler.log.Logger;
import org.robovm.compiler.target.ios.IOSTarget;
/**
* @author niklas
*
*/
public class ToolchainUtil {
private static String IOS_DEV_CLANG;
private static String IOS_SIM_CLANG;
private static String PNGCRUSH;
private static String PLUTIL;
private static String LIPO;
private static String PACKAGE_APPLICATION;
private static String TEXTUREATLAS;
private static String ACTOOL;
private static String IBTOOL;
private static String NM;
private static String OTOOL;
private static String FILE;
private static String getIOSDevClang() throws IOException {
if (IOS_DEV_CLANG == null) {
IOS_DEV_CLANG = findXcodeCommand("clang++", "iphoneos");
}
return IOS_DEV_CLANG;
}
private static String getIOSSimClang() throws IOException {
if (IOS_SIM_CLANG == null) {
IOS_SIM_CLANG = findXcodeCommand("clang++", "iphonesimulator");
}
return IOS_SIM_CLANG;
}
private static String getPngCrush() throws IOException {
if (PNGCRUSH == null) {
PNGCRUSH = findXcodeCommand("pngcrush", "iphoneos");
}
return PNGCRUSH;
}
private static String getTextureAtlas() throws IOException {
if (TEXTUREATLAS == null) {
TEXTUREATLAS = findXcodeCommand("TextureAtlas", "iphoneos");
}
return TEXTUREATLAS;
}
private static String getACTool() throws IOException {
if (ACTOOL == null) {
ACTOOL = findXcodeCommand("actool", "iphoneos");
}
return ACTOOL;
}
private static String getIBTool() throws IOException {
if (IBTOOL == null) {
IBTOOL = findXcodeCommand("ibtool", "iphoneos");
}
return IBTOOL;
}
private static String getPlutil() throws IOException {
if (PLUTIL == null) {
PLUTIL = findXcodeCommand("plutil", "iphoneos");
}
return PLUTIL;
}
private static String getLipo() throws IOException {
if (LIPO == null) {
LIPO = findXcodeCommand("lipo", "iphoneos");
}
return LIPO;
}
private static String getNm() throws IOException {
if (NM == null) {
NM = findXcodeCommand("nm", "iphoneos");
}
return NM;
}
private static String getOtool() throws IOException {
if(OTOOL == null) {
OTOOL = findXcodeCommand("otool", "iphoneos");
}
return OTOOL;
}
private static String getFile() throws IOException {
if (FILE == null) {
FILE = findXcodeCommand("file", "iphoneos");
}
return FILE;
}
private static String getPackageApplication() throws IOException {
if (PACKAGE_APPLICATION == null) {
PACKAGE_APPLICATION = findXcodeCommand("PackageApplication", "iphoneos");
}
return PACKAGE_APPLICATION;
}
private static void handleExecuteException(ExecuteException e) {
if (e.getExitValue() == 2) {
throw new IllegalArgumentException("No Xcode is selected. Is Xcode installed? "
+ "If yes, use 'sudo xcode-select -switch <path-to-xcode>' from a Terminal "
+ "to switch to the correct Xcode path.");
}
if (e.getExitValue() == 69) {
throw new IllegalArgumentException("You must agree to the Xcode/iOS license. "
+ "Please open Xcode once or run 'sudo xcrun clang' from a Terminal to agree to the terms.");
}
throw new IllegalArgumentException(e.getMessage());
}
public static String findXcodePath() throws IOException {
try {
String path = new Executor(Logger.NULL_LOGGER, "xcode-select").args("--print-path").execCapture();
File f = new File(path);
if (f.exists() && f.isDirectory()) {
if (new File(f, "Platforms").exists() && new File(f, "Toolchains").exists()) {
return path;
}
}
throw new IllegalArgumentException(String.format(
"The path '%s' does not appear to be a valid Xcode path. Use "
+ "'sudo xcode-select -switch <path-to-xcode>' from a Terminal "
+ "to switch to the correct Xcode path.", path));
} catch (ExecuteException e) {
handleExecuteException(e);
return null;
}
}
public static String findXcodeCommand(String cmd, String sdk) throws IOException {
try {
return new Executor(Logger.NULL_LOGGER, "xcrun").args("-sdk", sdk, "-f", cmd).execCapture();
} catch (ExecuteException e) {
handleExecuteException(e);
return null;
}
}
public static void pngcrush(Config config, File inFile, File outFile) throws IOException {
new Executor(config.getLogger(), getPngCrush()).args("-q", "-iphone", "-f", "0", inFile, outFile).exec();
}
public static void textureatlas(Config config, File inDir, File outDir) throws IOException {
List<String> opts = new ArrayList<String>();
int outputFormat = 1;
int maxTextureDimension = 1;
if (config.getTools() != null && config.getTools().getTextureAtlas() != null) {
TextureAtlas atlasConfig = config.getTools().getTextureAtlas();
outputFormat = 1 + atlasConfig.getOutputFormat().ordinal();
maxTextureDimension = 1 + atlasConfig.getMaximumTextureDimension().ordinal();
if (atlasConfig.usePowerOfTwo()) {
opts.add("-p");
}
}
new Executor(config.getLogger(), getTextureAtlas()).args(opts, "-f", outputFormat, "-s", maxTextureDimension,
inDir, outDir).exec();
}
public static void actool(Config config, File partialInfoPlist, File inDir, File outDir) throws IOException {
List<Object> opts = new ArrayList<>();
String appIconSetName = null;
String launchImagesName = null;
final String appiconset = "appiconset";
final String launchimage = "launchimage";
for (String fileName : inDir.list()) {
String ext = FilenameUtils.getExtension(fileName);
if (ext.equals(appiconset)) {
appIconSetName = FilenameUtils.getBaseName(fileName);
} else if (ext.equals(launchimage)) {
launchImagesName = FilenameUtils.getBaseName(fileName);
}
}
if (appIconSetName != null || launchImagesName != null) {
if (appIconSetName != null) {
opts.add("--app-icon");
opts.add(appIconSetName);
}
if (launchImagesName != null) {
opts.add("--launch-image");
opts.add(launchImagesName);
}
opts.add("--output-partial-info-plist");
opts.add(partialInfoPlist);
}
opts.add("--platform");
if (IOSTarget.isDeviceArch(config.getArch())) {
opts.add("iphoneos");
} else if (IOSTarget.isSimulatorArch(config.getArch())) {
opts.add("iphonesimulator");
}
String minOSVersion = config.getOs().getMinVersion();
if (config.getIosInfoPList() != null) {
String v = config.getIosInfoPList().getMinimumOSVersion();
if (v != null) {
minOSVersion = v;
}
}
new Executor(config.getLogger(), getACTool()).args("--output-format", "human-readable-text", opts,
"--minimum-deployment-target", minOSVersion, "--target-device", "iphone", "--target-device", "ipad",
"--compress-pngs", "--compile", outDir, inDir).exec();
}
public static void ibtool(Config config, File partialInfoPlist, File inFile, File outFile) throws IOException {
String minOSVersion = config.getOs().getMinVersion();
if (config.getIosInfoPList() != null) {
String v = config.getIosInfoPList().getMinimumOSVersion();
if (v != null) {
minOSVersion = v;
}
}
Executor executor = new Executor(config.getLogger(), getIBTool()).args("--target-device", "iphone",
"--target-device", "ipad", "--minimum-deployment-target", minOSVersion,
"--output-partial-info-plist", partialInfoPlist, "--auto-activate-custom-fonts", "--output-format",
"human-readable-text");
if (outFile.isDirectory()) {
executor.args("--compilation-directory", outFile);
} else {
executor.args("--compile", outFile);
}
executor.args(inFile).exec();
}
public static void compileStrings(Config config, File inFile, File outFile) throws IOException {
new Executor(config.getLogger(), getPlutil()).args("-convert", "binary1", inFile, "-o", outFile).exec();
}
public static void decompileXml(Config config, File inFile, File outFile) throws IOException {
new Executor(Logger.NULL_LOGGER, getPlutil()).args("-convert", "xml1", inFile, "-o", outFile).exec();
}
public static String nm(File file) throws IOException {
return new Executor(Logger.NULL_LOGGER, getNm()).args(file.getAbsolutePath()).execCapture();
}
public static String otool(File file) throws IOException {
return new Executor(new ConsoleLogger(false), getOtool()).args("-L", file.getAbsolutePath()).execCapture();
}
public static void lipo(Config config, File outFile, List<File> inFiles) throws IOException {
new Executor(config.getLogger(), getLipo()).args(inFiles, "-create", "-output", outFile).exec();
}
public static void lipoRemoveArchs(Config config, File inFile, File outFile, Arch ... archs) throws IOException {
List<Object> args = new ArrayList<>();
args.add(inFile);
for(Arch arch: archs) {
args.add("-remove");
args.add(arch.getClangName());
}
args.add("-output");
args.add(outFile);
new Executor(Logger.NULL_LOGGER, getLipo()).args(args).exec();
}
public static String lipoInfo(Config config, File inFile) throws IOException {
List<Object> args = new ArrayList<>();
args.add("-info");
args.add(inFile);
return new Executor(Logger.NULL_LOGGER, getLipo()).args(args).execCapture();
}
public static String file(File file) throws IOException {
return new Executor(Logger.NULL_LOGGER, getFile()).args(file).execCapture();
}
public static void packageApplication(Config config, File appDir, File outFile) throws IOException {
new Executor(config.getLogger(), getPackageApplication()).args(appDir, "-o", outFile).exec();
}
private static List<File> writeObjectsFiles(Config config, List<File> objectFiles, int maxObjectsPerFile,
boolean quote) throws IOException {
ArrayList<File> files = new ArrayList<>();
for (int i = 0, start = 0; start < objectFiles.size(); i++, start += maxObjectsPerFile) {
List<File> partition = objectFiles.subList(start, Math.min(objectFiles.size(), start + maxObjectsPerFile));
List<String> paths = new ArrayList<>();
for (File f : partition) {
paths.add((quote ? "\"" : "") + f.getAbsolutePath() + (quote ? "\"" : ""));
}
File objectsFile = new File(config.getTmpDir(), "objects" + i);
FileUtils.writeLines(objectsFile, paths, "\n");
files.add(objectsFile);
}
return files;
}
public static void link(Config config, List<String> args, List<File> objectFiles, List<String> libs, File outFile)
throws IOException {
boolean isDarwin = config.getOs().getFamily() == OS.Family.darwin;
/*
* The Xcode linker doesn't need paths with spaces to be quoted and will
* fail if we do quote. The Xcode linker will crash if we pass more than
* 65535 files in an objects file.
*
* The linker on Linux will fail if we don't quote paths with spaces.
*/
List<File> objectsFiles = writeObjectsFiles(config, objectFiles, isDarwin ? 0xffff : Integer.MAX_VALUE,
!isDarwin);
List<String> opts = new ArrayList<String>();
if (config.isDebug()) {
opts.add("-g");
}
if (isDarwin) {
opts.add("-arch");
opts.add(config.getArch().getClangName());
for (File objectsFile : objectsFiles) {
opts.add("-Wl,-filelist," + objectsFile.getAbsolutePath());
}
} else {
opts.add(config.getArch().is32Bit() ? "-m32" : "-m64");
for (File objectsFile : objectsFiles) {
opts.add("@" + objectsFile.getAbsolutePath());
}
}
opts.addAll(args);
new Executor(config.getLogger(), getCcPath(config)).args("-o", outFile, opts, libs).exec();
}
private static String getCcPath(Config config) throws IOException {
String ccPath = config.getOs().getFamily() == OS.Family.darwin ? "clang++" : "g++";
if (config.getCcBinPath() != null) {
ccPath = config.getCcBinPath().getAbsolutePath();
} else if (config.getOs() == OS.ios) {
if (config.getArch() == Arch.x86) {
ccPath = getIOSSimClang();
} else {
ccPath = getIOSDevClang();
}
}
return ccPath;
}
}