/** * Copyright (c) 2013 by JP Moresmau * This code is made available under the terms of the Eclipse Public License, * version 1.0 (EPL). See http://www.eclipse.org/legal/epl-v10.html */ package net.sf.eclipsefp.haskell.buildwrapper; import java.io.File; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import net.sf.eclipsefp.haskell.buildwrapper.types.BuildOptions; import net.sf.eclipsefp.haskell.buildwrapper.types.CabalImplDetails.SandboxType; import net.sf.eclipsefp.haskell.buildwrapper.util.BWText; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Display; /** * Helper for sandbox operations * @author JP Moresmau * */ public class SandboxHelper { /** * install dependencies of project using given facade in sandbox * @param f * @throws CoreException */ public static void installDeps(BWFacade f) throws CoreException{ installDeps(f, new HashSet<BWFacade>()); } /** * install a sandbox if needed * @param f */ public static void install(BWFacade f){ SandboxType st=f.getCabalImplDetails().getType(); if (SandboxType.CABAL.equals(st)){ IProject p=f.getProject(); // init unique sandbox if (f.getCabalImplDetails().isUniqueSandbox() && !f.getSandboxPath().exists()){ LinkedList<String> args=new LinkedList<>(); args.add("sandbox"); args.add("init"); args.add("--sandbox="+ f.getSandboxPath()); f.runCabal(args,"",f.getSandboxPath()); // just in case it's pointing to something else try { p.getFile("cabal.sandbox.config").delete(true, new NullProgressMonitor()); } catch (CoreException ce){ // noop } } if (!p.getFile("cabal.sandbox.config").exists()){ LinkedList<String> args=new LinkedList<>(); args.add("sandbox"); args.add("init"); args.addAll(f.getCabalImplDetails().getInitOptions()); f.runCabal(args,"",null); } } } public static void installDeps(BWFacade f,Set<BWFacade> processedFacades) throws CoreException{ if (f!=null && processedFacades.add(f)){ SandboxType st=f.getCabalImplDetails().getType(); IProject p=f.getProject(); switch (st){ case CABAL_DEV: { if (!f.getCabalImplDetails().isUniqueSandbox()){ Set<IProject> processed=new HashSet<>(); processed.add(p); for (IProject pR:p.getReferencedProjects()){ installDeps(f,pR,processed); } } LinkedList<String> args=new LinkedList<>(); args.add("install-deps"); // enable tests args.add("--enable-tests"); // enable benchmarks args.add("--enable-benchmarks"); // force reinstalls since we won't break anything outside of the sandbox args.add("--force-reinstalls"); args.addAll(f.getCabalImplDetails().getInstallOptions()); f.runCabal(args,null); break; } case CABAL: { install(f); //if (!f.getCabalImplDetails().isUniqueSandbox()){ Set<IProject> processed=new HashSet<>(); processed.add(p); for (IProject pR:p.getReferencedProjects()){ addSource(f,pR,processed); } //} // Cabal doesn't allow installing only-dependencies if the project has dependent projects in the same sandbox, so call installDeps on dependent if (f.getCabalImplDetails().isUniqueSandbox()){ IProject[] refs=p.getReferencingProjects(); if (refs.length>0){ for(IProject r:refs){ BWFacade rf=BuildWrapperPlugin.getFacade(r); if (rf!=null && !rf.isCanceled()){ installDeps(rf,processedFacades); } } return; } } LinkedList<String> args=new LinkedList<>(); args.add("install"); args.add("--only-dependencies"); // enable tests args.add("--enable-tests"); // enable benchmarks args.add("--enable-benchmarks"); // force reinstalls since we won't break anything outside of the sandbox args.add("--force-reinstalls"); args.addAll(f.getCabalImplDetails().getInstallOptions()); f.runCabal(args,null); break; } case NONE: break; } } } /** * install given project as a dependency on the given facade * @param sandboxFacade the facade * @param p the project * @param processed the set of already processed projects, in case of loops * @throws CoreException */ private static void installDeps(BWFacade sandboxFacade,IProject p,Set<IProject> processed) throws CoreException{ if (sandboxFacade.isCanceled()){ return; } if (processed.add(p)){ for (IProject pR:p.getReferencedProjects()){ installDeps(sandboxFacade,pR,processed); } if (sandboxFacade.isCanceled()){ return; } LinkedList<String> args=new LinkedList<>(); args.add("install"); args.add(p.getLocation().toOSString()); args.add("--force-reinstalls"); //if (SandboxType.CABAL_DEV.equals(sandboxFacade.getCabalImplDetails().getType())){ //if (s) args.addAll(sandboxFacade.getCabalImplDetails().getInstallOptions()); //} sandboxFacade.runCabal(args,null); } } /** * install given project as a dependency on the given facade * @param sandboxFacade the facade * @param p the project * @param processed the set of already processed projects, in case of loops * @throws CoreException */ private static void addSource(BWFacade sandboxFacade,IProject p,Set<IProject> processed) throws CoreException{ if (sandboxFacade.isCanceled() || !p.isAccessible()){ return; } if (processed.add(p)){ for (IProject pR:p.getReferencedProjects()){ addSource(sandboxFacade,pR,processed); } if (sandboxFacade.isCanceled()){ return; } Set<IProject> addSources=sandboxFacade.getCabalImplDetails().isUniqueSandbox() ?BuildWrapperPlugin.getAddSourceProjects() :sandboxFacade.getAddSourceProjects(); boolean lastAdd=addSources.contains(p); // add-source keeps track of modifications, so we only add once in the session to be sure if (!lastAdd){ LinkedList<String> args=new LinkedList<>(); args.add("sandbox"); args.add("add-source"); args.add(p.getLocation().toOSString()); sandboxFacade.runCabal(args,null); addSources.add(p); } } } /** * update all projects using the project wrapped in the given facade * @param f the facade * @throws CoreException */ public static void updateUsing(BWFacade f) throws CoreException{ if (f!=null){ SandboxType st=f.getCabalImplDetails().getType(); Set<IProject> processed=new HashSet<>(); switch (st){ case CABAL_DEV: if (!f.getCabalImplDetails().isUniqueSandbox()){ IProject p=f.getProject(); processed.add(p); for (IProject pR:p.getReferencingProjects()){ updateUsing(p,pR,processed); } } else { IProject p=f.getProject(); if (install(p,p,processed)){ for (IProject pR:p.getReferencingProjects()){ build(p,pR,processed); } } } break; case CABAL: // in Cabal sandboxes, we have automatic updates // either we build in the common sandbox, or we use addSource which maintain up to date packages // we let Eclipse manage this: if we build manually we don't want to build everything... // so we just restart the running processes IProject p=f.getProject(); for (IProject pR:p.getReferencingProjects()){ closeLongRunning(pR,processed); } break; case NONE: break; } } } /** * close long running buildwrapper processes * @param p * @param processed * @throws CoreException */ private static void closeLongRunning(IProject p,Set<IProject> processed) throws CoreException{ if (processed.add(p)){ BWFacade f=BuildWrapperPlugin.getFacade(p); if (f!=null){ if (f.isCanceled()){ return; } f.closeAllProcesses(); for (IProject pR:p.getReferencingProjects()){ closeLongRunning(pR,processed); } } } } /** * install the changedProject in p's sandbox * @param changedProject the changed project * @param p the project to update with the new version of the changed project * @param processed the set of already processed projects, in case of loops * @return true if the install in a sandbox happened */ private static boolean install(IProject changedProject,IProject p,Set<IProject> processed){ if (processed.add(p)){ BWFacade f=BuildWrapperPlugin.getFacade(p); if (f!=null && isSandboxed(f)){ if (f.isCanceled()){ return false; } LinkedList<String> args=new LinkedList<>(); args.add("install"); args.add(changedProject.getLocation().toOSString()); args.add("--force-reinstalls"); String expFlags=null; BWFacade changedF=BuildWrapperPlugin.getFacade(changedProject); if (changedF!=null){ expFlags=changedF.getFlags(); } f.runCabal(args,expFlags,null); // install the changed project with its own flags if (f.isCanceled()){ return false; } f.cleanGenerated(); // all generated files are wrong f.closeAllProcesses(); // GHC needs to reload the changes // rebuild if that's what the workspace wants if (f.isCanceled()){ return false; } return true; //f.clean(new NullProgressMonitor()); } } return false; } /** * build or configure a given project * @param p */ private static void build(IProject p){ BWFacade f=BuildWrapperPlugin.getFacade(p); if (f.isCanceled()){ return; } if (ResourcesPlugin.getWorkspace().isAutoBuilding()){ new JobFacade(f).build(new BuildOptions().setConfigure(true).setOutput(true).setRecompile(false)); } else { // just configure f.configure(new BuildOptions().setConfigure(true)); } } /** * update a given project using a changed project * @param changedProject the changed project * @param p the project to update with the new version of the changed project * @param processed the set of already processed projects, in case of loops * @throws CoreException */ private static void updateUsing(IProject changedProject,IProject p,Set<IProject> processed) throws CoreException{ if (install(changedProject,p,processed)){ build(p); for (IProject pR:p.getReferencingProjects()){ updateUsing(changedProject,pR,processed); } } } /** * build a project and the referencing projects * @param changedProject * @param p * @param processed * @throws CoreException */ private static void build(IProject changedProject,IProject p,Set<IProject> processed) throws CoreException{ if (processed.add(p)){ build(p); for (IProject pR:p.getReferencingProjects()){ build(changedProject,pR,processed); } } } /** * does a sandbox exists for the project wrapped in the given facade * @param f * @return */ public static boolean sandboxExists(BWFacade f){ if (f!=null){ SandboxType st=f.getCabalImplDetails().getType(); boolean uq=f.getCabalImplDetails().isUniqueSandbox(); switch (st){ case CABAL_DEV: return uq?new File(f.getCabalImplDetails().getSandboxPath()).exists() :f.getProject().getFolder(BWFacade.DIST_FOLDER_CABALDEV).exists(); case CABAL: return uq?new File(f.getCabalImplDetails().getSandboxPath()).exists() :f.getProject().getFolder(".cabal-sandbox").exists(); case NONE: return false; } } return false; } /** * do we have the proper reference to the sandbox? * @param f * @return */ public static boolean referencesSandbox(BWFacade f){ if (f!=null){ SandboxType st=f.getCabalImplDetails().getType(); if (SandboxType.CABAL.equals(st)){ return f.getProject().getFile("cabal.sandbox.config").exists(); } } return true; } public static void sandboxLocationChanged(BWFacade f){ if (f!=null){ SandboxType st=f.getCabalImplDetails().getType(); if (SandboxType.CABAL.equals(st)){ try { f.getProject().getFile("cabal.sandbox.config").delete(true, new NullProgressMonitor()); } catch (CoreException ce){ BuildWrapperPlugin.logError(BWText.error_sandbox,ce); } } } } /** * is the project sandboxed? * @param f * @return */ public static boolean isSandboxed(BWFacade f){ if (f!=null){ SandboxType st=f.getCabalImplDetails().getType(); switch (st){ case CABAL_DEV: case CABAL: return true; case NONE: return false; } } return false; } /** * listen to changes on project file and update dependencies accordingly * @author JP Moresmau * */ public static class ProjectReferencesChangeListener implements IResourceChangeListener{ /* (non-Javadoc) * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent) */ @Override public void resourceChanged( final IResourceChangeEvent event) { /** * if auto building, the building will see the project file has been changed and trigger installDeps accordingly */ if (!ResourcesPlugin.getWorkspace().isAutoBuilding()){ try { event.getDelta().accept(new ProjectReferencesChangeVisitor()); } catch (CoreException ce){ BuildWrapperPlugin.logError(BWText.error_sandbox,ce); } } } } /** * visitor for projects and definition file * @author JP Moresmau * */ private static class ProjectReferencesChangeVisitor implements IResourceDeltaVisitor { /* (non-Javadoc) * @see org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta) */ @Override public boolean visit(IResourceDelta delta) throws CoreException { if (delta.getResource() instanceof IProject){ if (IResourceDelta.CHANGED==delta.getKind()){ if (isSandboxed(BuildWrapperPlugin.getFacade((IProject)delta.getResource()))){ return true; } } return false; } else if (delta.getResource() instanceof IFile){ final IFile fi=(IFile)delta.getResource(); final IProject p=fi.getProject(); // description was changed if (fi.getProjectRelativePath().toPortableString().equals(IProjectDescription.DESCRIPTION_FILE_NAME)){ if (Display.findDisplay(Thread.currentThread())!=null){ String name=NLS.bind(BWText.job_sandbox_deps, p.getName()); Job installJob=new Job(name) { @Override protected IStatus run(IProgressMonitor arg0) { try { BWFacade f=BuildWrapperPlugin.getFacade(p); f.cabalFileChanged(); installDeps(f); } catch (CoreException ce){ BuildWrapperPlugin.logError(BWText.error_sandbox,ce); } return Status.OK_STATUS; } }; installJob.setRule(p); installJob.setPriority(Job.BUILD); installJob.schedule(); } else { try { BWFacade f=BuildWrapperPlugin.getFacade(p); f.cabalFileChanged(); installDeps(f); } catch (CoreException ce){ BuildWrapperPlugin.logError(BWText.error_sandbox,ce); } } } return false; } return true; } } }