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

org.robovm.compiler.target.framework.FrameworkTarget Maven / Gradle / Ivy

The newest version!
package org.robovm.compiler.target.framework;

import com.dd.plist.NSDictionary;
import com.dd.plist.PropertyListParser;
import org.apache.commons.io.FileUtils;
import org.robovm.compiler.clazz.Path;
import org.robovm.compiler.config.Arch;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.CpuArch;
import org.robovm.compiler.config.Environment;
import org.robovm.compiler.config.OS;
import org.robovm.compiler.target.AbstractTarget;
import org.robovm.compiler.target.ios.SDK;
import org.robovm.compiler.util.Executor;
import org.robovm.compiler.util.ToolchainUtil;
import org.robovm.compiler.util.XCFrameworkPlist;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.robovm.compiler.target.ios.IOSTarget.isDeviceArch;
import static org.robovm.compiler.target.ios.IOSTarget.isSimulatorArch;

public class FrameworkTarget extends AbstractTarget {

	private static final String TYPE = "framework";
	private static final String XC_TYPE = "xcframework";
	public static boolean matches(String candidate) {
		return (candidate.matches(TYPE) || candidate.matches((XC_TYPE)));
	}

    private OS os;
    private Arch arch;
    private SDK sdk;
	private boolean xcframework;

    public FrameworkTarget(String targetType) {
		this.xcframework = targetType.equals(XC_TYPE);
	}

	@Override
	public String getType() {
		return TYPE;
	}

	@Override
	public OS getOs() {
		return os;
	}

	@Override
	public Arch getArch() {
		return arch;
	}

    @Override
	public boolean canLaunch() {
		return false;
	}

    public List getSDKs() {
        if (isSimulatorArch(arch)) {
            return SDK.listSimulatorSDKs();
        } else {
            return SDK.listDeviceSDKs();
        }
    }

	public void init(Config paramConfig) {
		super.init(paramConfig);

		os = config.getOs();
        if (os == null)
            os = OS.getDefaultOS();

        arch = config.getArch();
        if (arch == null)
            arch = new Arch(CpuArch.getDefaultArch(), Environment.Native);

		if (os.getFamily() != OS.Family.darwin)
			throw new IllegalArgumentException("Frameworks can only be built for Darwin platforms");

		if (paramConfig.getInfoPList() == null)
			throw new IllegalArgumentException("Frameworks must have a Info.plist file");
		paramConfig.getIosInfoPList().parse(paramConfig.getProperties());

		String sdkVersion = config.getIosSdkVersion();

		List sdks = getSDKs();
        if (sdkVersion == null) {
            if (sdks.isEmpty()) {
                throw new IllegalArgumentException("No " + (isDeviceArch(arch) ? "device" : "simulator")
                        + " SDKs installed");
            }
            Collections.sort(sdks);
            this.sdk = sdks.get(sdks.size() - 1);
        }
        else {
            for (SDK sdk : sdks) {
                if (sdk.getVersion().equals(sdkVersion)) {
                    this.sdk = sdk;
                    break;
                }
            }
            if (sdk == null) {
                throw new IllegalArgumentException("No SDK found matching version string " + sdkVersion);
            }
        }

		if (!xcframework) {
			// regular framework
			// if building for fat binary -- check if it fits (e.g. if there is no arch and os conflict)
			Set duplicateCpuArches = config.getArchs().stream()
					.collect(Collectors.groupingBy(Arch::getCpuArch, Collectors.counting()))
					.entrySet()
					.stream().filter(e -> e.getValue() > 1)
					.map(Map.Entry::getKey)
					.collect(Collectors.toSet());
			if (!duplicateCpuArches.isEmpty()) {
				List duplicateArches = config.getArchs().stream()
						.filter(a -> duplicateCpuArches.contains(a.getCpuArch()))
						.sorted()
						.collect(Collectors.toList());
				throw new IllegalArgumentException("Wrong configuration: unable to create fat framework due duplicate CPU architectures: " +
						duplicateArches.stream().map(Arch::toString).collect(Collectors.joining(", ")) +
						". Use xcframework instead.");
			}
		}
	}

	@Override
	public String getInstallRelativeArchivePath(Path path) {
		return config.getImageName() + ".bundle/Resources/" + super.getInstallRelativeArchivePath(path);
	}

	@Override
	protected List getTargetExportedSymbols() {
		return Arrays.asList("JNI_*", "rvmInstantiateFramework", "rvmInitializeFrameworkWithJVM", "OBJC_CLASS_$_*");
	}

	private String getMinimumOSVersion() {
		String minimumOSVersion = config.getInfoPList().getMinimumOSVersion();
		if (minimumOSVersion != null)
			return minimumOSVersion;
		return config.getOs().getMinVersion();
	}

	@Override
	protected List getTargetCcArgs() {
		List ccArgs = new ArrayList();

		ccArgs.add("-stdlib=libc++");

		ccArgs.add("--target=" + config.getClangTriple(getMinimumOSVersion()));

		if (isDeviceArch(arch) && config.isEnableBitcode()) {
			// tells clang to keep bitcode while linking
			ccArgs.add("-fembed-bitcode");
		}

		ccArgs.add("-isysroot");
		ccArgs.add(sdk.getRoot().getAbsolutePath());

		ccArgs.add("-dynamiclib");
		ccArgs.add("-single_module");
		ccArgs.add("-compatibility_version");
		ccArgs.add("1");
		ccArgs.add("-current_version");
		ccArgs.add("1");

		ccArgs.add("-install_name");
		ccArgs.add(String.format("@rpath/%s.framework/%s", config.getImageName(), config.getImageName()));

		return ccArgs;
	}

	@Override
	protected List getTargetLibs() {
    	// adding framework support library
		String libSuffix = config.isUseDebugLibs() ? "-dbg" : "";
		return Collections.singletonList("-lrobovm-frameworksupport" + libSuffix);
	}


	/**
	 * Array of arches per one binary. Each entry contains arches that can be
	 * combined by lipo into single binary.
	 * Currently, these are separated with by Environment only, e.g.
	 * [
	 *     [x86_64, x86, arm64-simulator],   // simulator arches
	 *     [arm64, thumbv7]                  // native arches
	 * ]
	 */
	private Arch[][] xcFrameworkArches;

	private Arch[][] getXCFrameworkArches() {
		if (xcFrameworkArches == null) {
			List configArches = config.getArchs();
			if (configArches.size() == 1) {
				xcFrameworkArches = new Arch[][]{{configArches.get(0)}};
			} else {
				xcFrameworkArches = configArches.stream()
						.collect(Collectors.groupingBy(Arch::getEnv, Collectors.toList()))
						.values().stream()
						.map(arches -> arches.stream().sorted().toArray(Arch[]::new))
						.toArray(Arch[][]::new);
			}
		}
		return xcFrameworkArches;
	}

	@Override
	public void buildFat(Map slices) throws IOException {
		// lipo is not able to combine different platform variants of same CpuArch. E.g. macos-arm64, ios-arm64 and
		// ios-arm64-simulator can't be combined into single binary.
		// in this case we have to produce multiple fat binaries and create XCFramework

		if (xcframework) {
            // if building for xcframework -- just split by os and environment and build each library
			Arch[][] archesInBinaries = getXCFrameworkArches();
			Map slicesToConsume = new HashMap<>(slices);

			for (Arch[] archesInBinary: archesInBinaries) {
				List filesForLipo = new ArrayList<>(archesInBinary.length);
				for (Arch arch: archesInBinary) {
					File f = slicesToConsume.remove(arch);
					if (f == null)
						throw new IllegalStateException("Required arch " + arch + " is missing in slices!");
					filesForLipo.add(f);
				}

				if (archesInBinary.length == 1) {
					// single file, copy to expected location
					File f = filesForLipo.get(0);
					String archDir = archesInBinary[0].toString();
					File destDir = new File(config.getTmpDir(), archDir);
					destDir.mkdirs();
					File destFile = new File(destDir, getExecutable());
					if (!f.equals(destFile)) {
						FileUtils.copyFile(f, destFile);
						destFile.setExecutable(true, false);
					}
				} else {
					// build fat
					String archDir = Arrays.stream(archesInBinary)
							.map(Arch::getCpuArch)
							.map(CpuArch::getClangName)
							.collect(Collectors.joining("_"));
					archDir = archDir + archesInBinary[0].getEnv().asLlvmSuffix("-");
					config.getLogger().info("Building fat binary for archs " + archDir);

					File destDir = new File(config.getTmpDir(), archDir);
					destDir.mkdirs();
					File destFile = new File(destDir, getExecutable());
					ToolchainUtil.lipo(config, destFile, filesForLipo);
				}
			}
		} else {
            // build fat binary
            super.buildFat(slices);
		}
	}

	@Override
	protected void doInstall(File installDir, String image, File resourcesDir) throws IOException {
		if (xcframework) {
			File xcFrameworkDir = new File(installDir, image + ".xcframework");
			if (xcFrameworkDir.exists())
				FileUtils.deleteDirectory(xcFrameworkDir);
			xcFrameworkDir.mkdirs();

			XCFrameworkPlist.Builder builder = new XCFrameworkPlist.Builder();

			config.getLogger().info("Creating framework: %s", xcFrameworkDir);
			// writing multiple framework copies maintaining xcframework directory structure
			Arch[][] archesInBinaries = getXCFrameworkArches();
			for (Arch[] archesInBinary : archesInBinaries) {
				String identifier;
				if (archesInBinary.length == 1) {
					identifier = archesInBinary[0].toString();
				} else {
					// build fat
					identifier = Arrays.stream(archesInBinary)
							.map(Arch::getCpuArch)
							.map(CpuArch::getClangName)
							.collect(Collectors.joining("_"));
					identifier = identifier + archesInBinary[0].getEnv().asLlvmSuffix("-");
				}
				File frameworkArchDir = new File(xcFrameworkDir, identifier);
				File frameworkDir = new File(frameworkArchDir, image + ".framework");
				File dsymDir = new File(frameworkArchDir, "dSYMs/" + image + ".dSYM");;
				File executable;
				if (config.getArchs().size() == 1) {
					// single arch framework. it was build without invoking buildFat() so binary is not copied
					// to arch-specific location and to be picked from build tmp folder
					executable = new File(config.getTmpDir(), getExecutable());
				} else {
					executable = new File(new File(config.getTmpDir(), identifier), getExecutable());
				}
				config.getLogger().info("Installing " + identifier + "framework slice: %s", frameworkDir);
				installFramework(frameworkDir, dsymDir, executable, image);

				XCFrameworkPlist.Library library = new XCFrameworkPlist.Library(identifier,
						image + ".framework", config.getOs(),
						Arrays.stream(archesInBinary).map(Arch::getCpuArch).toArray(CpuArch[]::new),
						archesInBinary[0].getEnv(), "dSYMs");
				builder.addLibrary(library);
			}

			XCFrameworkPlist plist = builder.build();
			File infoPlist = new File(xcFrameworkDir, "Info.plist");
			config.getLogger().info("Installing XCFramework/Info.plist to: %s", infoPlist);
			plist.writeTo(infoPlist);
		} else {
			// classic framework
			File executable = new File(config.getTmpDir(), getExecutable());
			File frameworkDir = new File(installDir, image + ".framework");
			File dsymDir = new File(installDir, image + ".dSYM");
			config.getLogger().info("Creating framework: %s", frameworkDir);
			installFramework(frameworkDir, dsymDir, executable, image);
		}
	}

	private void installFramework(File frameworkDir, File dsymDir, File binary, String image) throws IOException {
		if (frameworkDir.exists())
			FileUtils.deleteDirectory(frameworkDir);
		frameworkDir.mkdirs();

		// copy executable if required
		File executable = new File(frameworkDir, image);
		if (!binary.equals(executable)) {
			FileUtils.copyFile(binary, executable);
			executable.setExecutable(true, false);
		}

		File bundleDir = new File(frameworkDir, image + ".bundle");
		bundleDir.mkdirs();

		File bundleResourcesDir = new File(bundleDir, "Resources");
		bundleResourcesDir.mkdirs();

		super.doInstall(frameworkDir, executable, bundleResourcesDir);

		config.getLogger().info("Creating framework symbol directory: %s", dsymDir);
		if (dsymDir.exists())
			FileUtils.deleteDirectory(dsymDir);
		dsymDir.mkdirs();
		ToolchainUtil.generateDsym(config, dsymDir, executable);

		if (!config.isDebug()) {
			config.getLogger().info("Striping framework binary: %s", executable);
			new Executor(config.getLogger(), "xcrun").args("strip", "-x", executable).exec();
		}
		// remove bitcode to minimize binary size if not required
		if (!config.isEnableBitcode()) {
			config.getLogger().info("Striping bitcode from binary: %s", executable);
			stripBitcode(executable);
		}

		NSDictionary infoPlist = config.getInfoPList().getDictionary();
		if (infoPlist.objectForKey("MinimumOSVersion") == null)
			infoPlist.put("MinimumOSVersion", config.getOs().getMinVersion());

		File infoPlistBin = new File(frameworkDir, "Info.plist");
		config.getLogger().info("Installing Info.plist to: %s", infoPlistBin);
		PropertyListParser.saveAsBinary(infoPlist, infoPlistBin);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy