package com.redhat.ceylon.eclipse.android.plugin; import static com.redhat.ceylon.eclipse.java2ceylon.Java2CeylonProxies.modelJ2C; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.file.FileSystems; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; import org.eclipse.core.resources.IBuildConfiguration; import org.eclipse.core.resources.IBuildContext; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jdt.core.IClasspathContainer; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.ui.util.CoreUtility; import com.redhat.ceylon.cmr.api.ArtifactContext; import com.redhat.ceylon.cmr.api.RepositoryManager; import com.redhat.ceylon.cmr.ceylon.CeylonUtils.CeylonRepoManagerBuilder; import com.redhat.ceylon.cmr.ceylon.LegacyImporter; import com.redhat.ceylon.common.FileUtil; import com.redhat.ceylon.common.Versions; import com.redhat.ceylon.compiler.java.runtime.model.TypeDescriptor; import com.redhat.ceylon.eclipse.core.builder.CeylonBuilder; import com.redhat.ceylon.eclipse.core.builder.CeylonBuilder.BooleanHolder; import com.redhat.ceylon.eclipse.core.builder.CeylonBuilder.CeylonBuildHook; import com.redhat.ceylon.eclipse.core.builder.ICeylonBuildHookProvider; import com.redhat.ceylon.eclipse.core.classpath.CeylonClasspathUtil; import com.redhat.ceylon.eclipse.ui.CeylonPlugin; import com.redhat.ceylon.ide.common.model.CeylonProject; import com.redhat.ceylon.ide.common.model.CeylonProjectConfig; import com.redhat.ceylon.ide.common.model.IdeModule; import com.redhat.ceylon.model.cmr.ArtifactResult; import com.redhat.ceylon.model.loader.JvmBackendUtil; import com.redhat.ceylon.model.typechecker.model.Module; import ceylon.interop.java.CeylonIterable; import ceylon.interop.java.JavaIterable; @SuppressWarnings("restriction") public class AndroidBuildHookProvider implements ICeylonBuildHookProvider { private static final class AndroidCeylonBuildHook extends CeylonBuildHook { public static final String CEYLON_RENAMED_CARS_FOLDER = ".renamed-cars-for-android"; public static final String CEYLON_RENAMED_CARS_CPC_NAME = CeylonAndroidPlugin.PLUGIN_ID + ".RENAMED_CARS"; private static final String[] NECESSARY_CEYLON_RUNTIME_LIBRARIES = new String[] { "com.redhat.ceylon.module-resolver-"+Versions.CEYLON_VERSION_NUMBER+".jar", "com.redhat.ceylon.common-"+Versions.CEYLON_VERSION_NUMBER+".jar", "com.redhat.ceylon.model-"+Versions.CEYLON_VERSION_NUMBER+".jar", "com.redhat.ceylon.langtools.classfile-"+Versions.CEYLON_VERSION_NUMBER+".jar", }; private static Path CPC_PATH = new Path(CEYLON_RENAMED_CARS_CPC_NAME + "/default"); boolean configFileChanged = false; boolean areModulesChanged = false; boolean hasAdtNature = false; boolean hasAndMoreNature = false; boolean isReentrantBuild = false; boolean isFullBuild = false; WeakReference<IProgressMonitor> monitorRef = null; WeakReference<IProject> projectRef = null; private IProgressMonitor getMonitor() { if (monitorRef != null) { return monitorRef.get(); } return null; } private IProject getProject() { if (projectRef != null) { return projectRef.get(); } return null; } private boolean hasAndroidNature() { return hasAdtNature || hasAndMoreNature; } public class AndroidRenamedCarsContainer implements IClasspathContainer { private IClasspathEntry[] classpathEntries = new IClasspathEntry[0]; @Override public IPath getPath() { return CPC_PATH; } @Override public int getKind() { return IClasspathContainer.K_APPLICATION; } @Override public String getDescription() { return null; } @Override public IClasspathEntry[] getClasspathEntries() { return classpathEntries; } public void resetClasspathEntries() throws CoreException { ArrayList<IClasspathEntry> entries = new ArrayList<>(); IFolder renamedCarsFolder = getProject().getFolder(CEYLON_RENAMED_CARS_FOLDER); renamedCarsFolder.refreshLocal(IResource.DEPTH_ONE, getMonitor()); if (! renamedCarsFolder.exists()) { CoreUtility.createDerivedFolder(renamedCarsFolder, true, true, getMonitor()); } if (renamedCarsFolder.isHidden()) { renamedCarsFolder.setHidden(false); } for (IResource member : renamedCarsFolder.members(IContainer.INCLUDE_HIDDEN | IContainer.INCLUDE_PHANTOMS)) { member.delete(true, getMonitor()); } IJavaProject javaProject = JavaCore.create(getProject()); for (IClasspathContainer ceylonContainer : CeylonClasspathUtil.getCeylonClasspathContainers(javaProject)) { for (IClasspathEntry ceylonEntry : ceylonContainer.getClasspathEntries()) { if (ceylonEntry.getPath().getFileExtension().equalsIgnoreCase("CAR")) { java.nio.file.Path sourcePath = FileSystems.getDefault().getPath(ceylonEntry.getPath().toOSString()); String ceylonCarName = ceylonEntry.getPath().lastSegment(); String jarName = ceylonCarName.substring(0, ceylonCarName.length()-3) + "jar"; java.nio.file.Path destinationPath = FileSystems.getDefault().getPath(renamedCarsFolder.getLocation().toOSString(), jarName); try { Files.copy(sourcePath, destinationPath); } catch (IOException e) { CeylonAndroidPlugin.logError("Could not copy a ceylon jar to the android libs directory", e); } } } } renamedCarsFolder.refreshLocal(IResource.DEPTH_INFINITE, getMonitor()); for (IResource resource : renamedCarsFolder.members(IContainer.INCLUDE_HIDDEN | IContainer.INCLUDE_PHANTOMS)) { if (resource.getFileExtension().equals("jar")) { IClasspathEntry entry = JavaCore.newLibraryEntry(resource.getFullPath(), null, null); entries.add(entry); } } for (String requiredPath : CeylonPlugin.getRequiredJars(NECESSARY_CEYLON_RUNTIME_LIBRARIES)) { IClasspathEntry entry = JavaCore.newLibraryEntry(new Path(requiredPath), null, null); entries.add(entry); } classpathEntries = new IClasspathEntry[entries.size()]; classpathEntries = entries.toArray(classpathEntries); } } private AndroidRenamedCarsContainer getRenamedCarsCPC(IProject project) throws CoreException { IJavaProject javaProject = JavaCore.create(project); IClasspathEntry[] oldEntries = javaProject.getRawClasspath(); for (IClasspathEntry entry : oldEntries) { if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER && entry.getPath().segment(0).equals(CEYLON_RENAMED_CARS_CPC_NAME)) { IClasspathContainer classpathContainer = JavaCore.getClasspathContainer(CPC_PATH, javaProject); if (classpathContainer instanceof AndroidRenamedCarsContainer) { return (AndroidRenamedCarsContainer) classpathContainer; } else { AndroidRenamedCarsContainer cpc = new AndroidRenamedCarsContainer(); cpc.resetClasspathEntries(); JavaCore.setClasspathContainer(cpc.getPath(), new IJavaProject[] { javaProject } , new IClasspathContainer[] { cpc }, getMonitor()); return cpc; } } } AndroidRenamedCarsContainer cpc = new AndroidRenamedCarsContainer(); cpc.resetClasspathEntries(); IClasspathEntry[] newEntries = new IClasspathEntry[oldEntries.length + 1]; System.arraycopy(oldEntries, 0, newEntries, 0, oldEntries.length); IClasspathEntry renamedCarsContainerEntry = JavaCore.newContainerEntry(cpc.getPath(), true); newEntries[oldEntries.length] = renamedCarsContainerEntry; javaProject.setRawClasspath(newEntries, getMonitor()); JavaCore.setClasspathContainer(cpc.getPath(), new IJavaProject[] { javaProject } , new IClasspathContainer[] { cpc }, getMonitor()); return cpc; } private String getProjectAndroidVersion(IProject project) { try { java.io.File projectProperties = new java.io.File(project.getLocation().toFile(), "project.properties"); if (projectProperties.exists()) { Properties props = new Properties(); props.load(new FileReader(projectProperties)); String target = props.getProperty("target"); if (target.indexOf('-') > 0) { return target.split("-")[1]; } } } catch (IOException e) { CeylonAndroidPlugin.logError("Exception occured in the CeylonAndroidPlugin", e); } return null; } private String getSdkHome() { IEclipsePreferences preferences = InstanceScope.INSTANCE .getNode("org.eclipse.andmore"); if (preferences != null) { return preferences.get("org.eclipse.andmore.sdk", null); } return null; } private java.io.File getAndroidSupportInstalledFolder() { String sdkHome = getSdkHome(); if (sdkHome != null) { java.io.File supportFolder = new java.io.File(new java.io.File(new java.io.File(sdkHome, "extras"), "android"), "support"); if (supportFolder.exists()) { return supportFolder; } } return null; } private String getAndroidSupportInstalledVersion() { java.io.File supportFolder = getAndroidSupportInstalledFolder(); if (supportFolder != null) { try { java.io.File supportSourceProperties = new java.io.File(supportFolder, "source.properties"); if (supportSourceProperties.exists()) { Properties props = new Properties(); props.load(new FileReader(supportSourceProperties)); return props.getProperty("Pkg.Revision"); } } catch (IOException e) { CeylonAndroidPlugin.logError("Exception occured in the CeylonAndroidPlugin", e); } } return null; } private File getCeylonAndroidRepositoryFile(IProject project) { java.io.File androidCeylonRepository = new java.io.File(project.getLocation().toFile(), "androidCeylonRepository"); return androidCeylonRepository; } private List<File> getFilesInClasspathContainer(String cpcName) { IJavaProject javaProject = JavaCore.create(getProject()); ArrayList<File> files = new ArrayList<File>(); try { for (IClasspathEntry entry : javaProject.getRawClasspath()) { if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER && entry.getPath().segment(0).equals(cpcName)) { IClasspathContainer container = JavaCore.getClasspathContainer(entry.getPath(), javaProject); if (container != null){ for (IClasspathEntry cpcEntry : container.getClasspathEntries()) { if (cpcEntry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { files.add(cpcEntry.getPath().toFile()); } } } break; } } } catch (JavaModelException e) { CeylonAndroidPlugin.logError("", e); } return files; } @Override protected void startBuild(int kind, @SuppressWarnings("rawtypes") Map args, IProject project, IBuildConfiguration config, IBuildContext context, IProgressMonitor monitor) throws CoreException { try { hasAdtNature = project.hasNature("com.android.ide.eclipse.adt.AndroidNature"); } catch (CoreException e) { hasAdtNature= false; } try { hasAdtNature = project.hasNature("org.eclipse.andmore.AndroidNature"); } catch (CoreException e) { hasAndMoreNature= false; } configFileChanged = false; areModulesChanged = false; if (!hasAndroidNature()) { return; } monitorRef = new WeakReference<IProgressMonitor>(monitor); projectRef = new WeakReference<IProject>(project); isReentrantBuild = args.containsKey(CeylonBuilder.BUILDER_ID + ".reentrant"); CeylonProjectConfig projectConfig = modelJ2C().ceylonConfig(getProject()); ceylon.language.String jdkProvider = projectConfig.getProjectJdkProvider(); String androidVersion = getProjectAndroidVersion(getProject()); String newJdkProvider = null; if (androidVersion != null) { newJdkProvider = "android/" + androidVersion; } if (jdkProvider == null && newJdkProvider != null) { projectConfig.setProjectJdkProvider(new ceylon.language.String(newJdkProvider)); configFileChanged = true; } else { String existingJdkProvider = jdkProvider.toString(); if (newJdkProvider != null && ! newJdkProvider.equals(existingJdkProvider)) { projectConfig.setProjectJdkProvider(new ceylon.language.String(newJdkProvider)); configFileChanged = true; } } final TypeDescriptor stringTD = TypeDescriptor.klass(ceylon.language.String.class); ceylon.language.String ceylonAndroidRepository = new ceylon.language.String("./" + getCeylonAndroidRepositoryFile(getProject()).getName()); if (! projectConfig.getProjectLocalRepos().contains( ceylonAndroidRepository)) { ArrayList<ceylon.language.String> newProjectLoclRepos = new ArrayList<>(); Iterable<ceylon.language.String> javaIter = new JavaIterable<ceylon.language.String>( stringTD, projectConfig.getProjectLocalRepos()); newProjectLoclRepos.add(ceylonAndroidRepository); for (ceylon.language.String oldRepo : javaIter) { newProjectLoclRepos.add(oldRepo); } projectConfig.setProjectLocalRepos(new CeylonIterable<ceylon.language.String>(stringTD, newProjectLoclRepos).sequence()); configFileChanged = true; } if (configFileChanged) { projectConfig.save(); } /* if (hasAndroidNature()) { IJavaProject javaProject =JavaCore.create(project); boolean CeylonCPCFound = false; IMarker[] buildMarkers = project.findMarkers(IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, true, DEPTH_ZERO); for (IMarker m: buildMarkers) { if (CeylonAndroidPlugin.PLUGIN_ID.equals(m.getAttribute(IMarker.SOURCE_ID))) { m.delete(); } } for (IClasspathEntry entry : javaProject.getRawClasspath()) { if (CeylonClasspathUtil.isProjectModulesClasspathContainer(entry.getPath())) { CeylonCPCFound = true; } else { IPath containerPath = entry.getPath(); int size = containerPath.segmentCount(); if (size > 0) { if (containerPath.segment(0).equals("com.android.ide.eclipse.adt.LIBRARIES") || containerPath.segment(0).equals("com.android.ide.eclipse.adt.DEPENDENCIES")) { if (! CeylonCPCFound) { //if the ClassPathContainer is missing, add an error IMarker marker = project.createMarker(IJavaModelMarker.BUILDPATH_PROBLEM_MARKER); marker.setAttribute(IMarker.SOURCE_ID, CeylonAndroidPlugin.PLUGIN_ID); marker.setAttribute(IMarker.MESSAGE, "Invalid Java Build Path for project " + project.getName() + " : " + "The Ceylon libraries should be set before the Android libraries in the Java Build Path. " + "Move down the 'Android Private Libraries' and 'Android Dependencies' after the Ceylon Libraries " + "in the 'Order and Export' tab of the 'Java Build Path' properties page."); marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH); marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); marker.setAttribute(IMarker.LOCATION, "Java Build Path Order"); throw new CoreException(new Status(IStatus.CANCEL, CeylonAndroidPlugin.PLUGIN_ID, IResourceStatus.OK, "Build cancelled because of invalid build path", null)); } } } } } } */ } String[][] supportLibraries = new String[][] { new String[] { "android-support-annotations.jar", "support-annotations" }, new String[] { "android-support-customtabs.jar", "customtabs" }, new String[] { "android-support-design.jar", "design" }, new String[] { "android-support-multidex.jar", "multidex" }, new String[] { "android-support-percent.jar", "percent" }, new String[] { "android-support-recommendation.jar", "recommendation" }, new String[] { "android-support-v4.jar", "support-v4" }, new String[] { "android-support-v7-appcompat.jar", "appcompat-v7", "support-v4.jar"}, new String[] { "android-support-v7-cardview.jar", "cardview-v7"}, new String[] { "android-support-v7-gridlayout.jar", "gridlayout-v7"}, new String[] { "android-support-v7-mediarouter.jar", "mediarouter-v7"}, new String[] { "android-support-v7-palette.jar", "palette-v7"}, new String[] { "android-support-v7-preference.jar", "preference-v7"}, new String[] { "android-support-v7-recyclerview.jar", "recyclerview-v7"}, new String[] { "android-support-v13.jar", "support-v13" }, new String[] { "android-support-v14-preference.jar", "preference-v14"}, new String[] { "android-support-v17-leanback.jar", "leanback-v17"}, new String[] { "android-support-v17-preference-leanback.jar", "preference-leanback-v17"} }; @Override protected void deltasAnalyzed(List<IResourceDelta> currentDeltas, BooleanHolder sourceModified, BooleanHolder mustDoFullBuild, BooleanHolder mustResolveClasspathContainer, boolean mustContinueBuild) { if (! mustContinueBuild || ! hasAndroidNature()) { return; } CeylonBuilder.waitForUpToDateJavaModel(10000, getProject(), getMonitor()); CeylonProjectConfig projectConfig = modelJ2C().ceylonConfig(getProject()); if (configFileChanged) { mustResolveClasspathContainer.value = true; mustDoFullBuild.value = true; } if (mustResolveClasspathContainer.value || mustDoFullBuild.value) { String androidVersion = getProjectAndroidVersion(getProject()); String androidSupportVersion = getAndroidSupportInstalledVersion(); if (androidVersion != null) { File androidCeylonRepo = getCeylonAndroidRepositoryFile(getProject()); if (androidCeylonRepo.exists()) { FileUtil.delete(androidCeylonRepo); } RepositoryManager outputRepoMgr = new CeylonRepoManagerBuilder() .cwd(getProject().getLocation().toFile()) .outRepo("./" + androidCeylonRepo.getName()) .buildOutputManager(); androidCeylonRepo.mkdirs(); for (File archive : getFilesInClasspathContainer("org.eclipse.andmore.ANDROID_FRAMEWORK")) { if (archive.getName().equals("android.jar")) { LegacyImporter importer = new LegacyImporter("android", androidVersion, archive, outputRepoMgr, outputRepoMgr); importer.publish(); break; } } if (androidSupportVersion != null) { for (File archive : getFilesInClasspathContainer("org.eclipse.andmore.LIBRARIES")) { if (archive.getName().startsWith("android-support-")) { for (String[] libDesc : supportLibraries) { if (libDesc[0].equals(archive.getName())) { String moduleName = "com.android.support." + libDesc[1]; File propsFile = null; FileWriter writer = null; try { propsFile = File.createTempFile(moduleName, ".properties"); writer = new FileWriter(propsFile); writer.write("+android=" + androidVersion); if (libDesc.length > 2) { for (int i = 2; i<libDesc.length; i++) { String dep = libDesc[i]; writer.write("+" + dep + "=" + androidSupportVersion); } } writer.flush(); writer.close(); LegacyImporter importer = new LegacyImporter(moduleName, androidSupportVersion, archive, outputRepoMgr, outputRepoMgr); importer.moduleDescriptor(propsFile); importer.publish(); break; } catch (IOException e) { CeylonAndroidPlugin.logError("", e); } finally { if (writer!= null) { try { writer.close(); } catch (IOException e) {} } if (propsFile != null) { propsFile.delete(); propsFile.deleteOnExit(); } } } } } } } } } } @Override protected void setAndRefreshClasspathContainer() { areModulesChanged = true; } @Override protected void doFullBuild() { isFullBuild = true; } @Override protected void afterGeneratingBinaries() { IProject project = getProject(); if (project == null) { return; } if (! isReentrantBuild && hasAndroidNature()) { try { AndroidRenamedCarsContainer renamedCarsContainer = getRenamedCarsCPC(project); if (isFullBuild || areModulesChanged) { renamedCarsContainer.resetClasspathEntries(); } @SuppressWarnings("rawtypes") CeylonProject ceylonProject = modelJ2C().ceylonModel().getProject(getProject()); ArrayList<ArtifactResult> moduleArtifacts = new ArrayList<>(); RepositoryManager repositoryManager = ceylonProject.newRepositoryManagerBuilder(true).buildManager(); for (Module m : CeylonBuilder.getProjectDeclaredSourceModules(getProject())) { ArtifactContext artifactContext = new ArtifactContext(null, m.getNameAsString(), m.getVersion(), ArtifactContext.CAR); ArtifactResult result = repositoryManager.getArtifactResult(artifactContext); if (result != null) { moduleArtifacts.add(result); } else { CeylonAndroidPlugin.logError("Generated Car " + artifactContext + " not found in output repository manager", null); } } for (Module m : CeylonBuilder.getProjectExternalModules(getProject())) { if (m instanceof IdeModule) { @SuppressWarnings("rawtypes") IdeModule ideModule = (IdeModule) m; if (! (ideModule.getIsCeylonBinaryArchive() || ideModule.getIsJavaBinaryArchive())) { continue; } ArtifactContext artifactContext = new ArtifactContext(null, m.getNameAsString(), m.getVersion(), ArtifactContext.CAR, ArtifactContext.JAR); ArtifactResult result = repositoryManager.getArtifactResult(artifactContext); if (result != null) { moduleArtifacts.add(result); } else { CeylonAndroidPlugin.logError("Dependency archive " + artifactContext + " not found in output repository manager", null); } } } java.io.File explodedDir = CeylonBuilder.getCeylonClassesOutputDirectory(getProject()); JvmBackendUtil.writeStaticMetamodel( explodedDir, moduleArtifacts, ceylonProject.getModelLoader().getJdkProvider(), "ANDROID-META-INF"); } catch(Exception e) { CeylonAndroidPlugin.logError("Exception in Ceylon Android Plugin", e); } } } @Override protected void endBuild() { configFileChanged = false; areModulesChanged = false; hasAdtNature = false; hasAndMoreNature = false; isReentrantBuild = false; isFullBuild = false; monitorRef = null; projectRef = null; } } private static CeylonBuildHook buildHook = new AndroidCeylonBuildHook(); @Override public CeylonBuildHook getHook() { return buildHook; } }