/*******************************************************************************
* Copyright (c) 2013 Vlad Dumitrescu and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available
* at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Vlad Dumitrescu
*******************************************************************************/
package org.erlide.core.internal.builder;
import static com.google.common.collect.Lists.newArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
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.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.osgi.util.NLS;
import org.erlide.backend.BackendCore;
import org.erlide.backend.api.BackendException;
import org.erlide.backend.api.IBackend;
import org.erlide.core.builder.BuildNotifier;
import org.erlide.core.builder.BuildResource;
import org.erlide.core.builder.BuilderHelper;
import org.erlide.core.builder.BuilderHelper.SearchVisitor;
import org.erlide.core.builder.BuilderMessages;
import org.erlide.core.builder.CompilerOptions;
import org.erlide.engine.ErlangEngine;
import org.erlide.engine.MarkerUtils;
import org.erlide.engine.model.ErlModelException;
import org.erlide.engine.model.builder.BuilderProperties;
import org.erlide.engine.model.erlang.SourceKind;
import org.erlide.engine.model.root.ErlangProjectProperties;
import org.erlide.engine.model.root.IErlModel;
import org.erlide.engine.model.root.IErlModule;
import org.erlide.engine.model.root.IErlProject;
import org.erlide.runtime.rpc.RpcFuture;
import org.erlide.util.ErlLogger;
import org.erlide.util.SystemConfiguration;
import com.ericsson.otp.erlang.OtpErlangList;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
public class InternalBuilder extends ErlangBuilder {
private final BuilderHelper helper = new BuilderHelper();
IResourceDelta delta;
public void setDelta(final IResourceDelta delta) {
this.delta = delta;
}
@Override
public IProject[] build(final BuildKind kind, final IErlProject erlProject,
final BuildNotifier notifier) throws CoreException {
final long time = System.currentTimeMillis();
final IProject project = erlProject.getWorkspaceProject();
if (project == null || !project.isAccessible()) {
return null;
}
if (BuilderHelper.isDebugging()) {
ErlLogger.trace("build", "Start " + project.getName() + ": " + kind);
}
try {
initializeBuilder(notifier);
// TODO validate source and include directories
final ErlangProjectProperties properties = erlProject.getProperties();
final IPath out = properties.getOutputDir();
final IResource outr = project.findMember(out);
if (outr != null) {
try {
outr.setDerived(true, null);
outr.refreshLocal(IResource.DEPTH_ZERO, null);
} catch (final CoreException e) {
// ignore it
}
}
if (delta != null && delta.getAffectedChildren().length != 0) {
handleAppFile(project,
project.getLocation().toPortableString() + "/" + out,
properties.getSourceDirs());
}
handleErlangFiles(erlProject, project, kind, delta, notifier);
} catch (final OperationCanceledException e) {
if (BuilderHelper.isDebugging()) {
ErlLogger.debug("Build of " + project.getName() + " was canceled.");
}
} catch (final Exception e) {
ErlLogger.error(e);
final String msg = NLS.bind(BuilderMessages.build_inconsistentProject,
e.getLocalizedMessage(), e.getClass().getName());
MarkerUtils.createProblemMarker(project, null, msg, 0,
IMarker.SEVERITY_ERROR);
} finally {
cleanup(notifier);
if (BuilderHelper.isDebugging()) {
ErlLogger.trace("build", " Done " + project.getName() + " took "
+ Long.toString(System.currentTimeMillis() - time));
}
}
return null;
}
@Override
public void clean(final IErlProject erlProject, final BuildNotifier notifier) {
final IProject currentProject = erlProject.getWorkspaceProject();
if (currentProject == null || !currentProject.isAccessible()) {
return;
}
if (BuilderHelper.isDebugging()) {
ErlLogger.trace("build", "Cleaning " + currentProject.getName() //$NON-NLS-1$
+ " @ " + new Date(System.currentTimeMillis()));
}
try {
initializeBuilder(notifier);
MarkerUtils.removeProblemMarkersFor(currentProject);
final IFolder bf = currentProject
.getFolder(erlProject.getProperties().getOutputDir());
if (bf.exists()) {
cleanupOutput(bf, notifier);
}
} catch (final Exception e) {
ErlLogger.error(e);
final String msg = NLS.bind(BuilderMessages.build_inconsistentProject,
e.getLocalizedMessage(), e.getClass().getName());
MarkerUtils.createProblemMarker(currentProject, null, msg, 0,
IMarker.SEVERITY_ERROR);
} finally {
cleanup(notifier);
if (BuilderHelper.isDebugging()) {
ErlLogger.debug("Finished cleaning " + currentProject.getName() //$NON-NLS-1$
+ " @ " + new Date(System.currentTimeMillis()));
}
}
}
private void cleanupOutput(final IFolder folder, final BuildNotifier notifier)
throws CoreException {
final IResource[] beams = folder.members();
notifier.beginTask("Cleaning Erlang files", beams.length);
if (beams.length > 0) {
final float ndelta = 100.0f / beams.length;
for (final IResource element : beams) {
if ("beam".equals(element.getFileExtension())) {
final IResource source = findCorrespondingSource(element);
if (source != null) {
element.delete(true, notifier.monitor);
}
notifier.updateProgressDelta(ndelta);
}
if ("app".equals(element.getFileExtension())) {
final IResource source = findCorrespondingSource(element);
if (source != null) {
element.delete(true, notifier.monitor);
}
}
}
}
}
private void handleErlangFiles(final IErlProject erlProject,
final @NonNull IProject project, final BuildKind kind,
final IResourceDelta resourceDelta, final BuildNotifier notifier)
throws CoreException, BackendException {
final OtpErlangList compilerOptions = CompilerOptions.get(project);
final Set<BuildResource> resourcesToBuild = getResourcesToBuild(kind, project,
resourceDelta, notifier);
final int n = resourcesToBuild.size();
if (n == 0) {
return;
}
if (erlProject == null) {
return;
}
// if (BuilderHelper.isDebugging()) {
ErlLogger.debug("Will compile %d resource(s)", Integer.valueOf(n));
// }
final IBackend backend = BackendCore.getBackendManager()
.getBuildBackend(erlProject);
if (backend == null) {
final String message = "No backend with the required "
+ "version could be found. Can't build.";
MarkerUtils.createProblemMarker(project, null, message, 0,
IMarker.SEVERITY_ERROR);
throw new BackendException(message);
}
final IErlModel model = ErlangEngine.getInstance().getModel();
backend.addProjectPath(model.findProject(project));
notifier.setProgressPerCompilationUnit(1.0f / n);
final Map<RpcFuture, IResource> results = new HashMap<>();
for (final BuildResource bres : resourcesToBuild) {
notifier.checkCancel();
final IResource resource = bres.getResource();
MarkerUtils.deleteMarkers(resource);
notifier.aboutToCompile(resource);
if ("erl".equals(resource.getFileExtension())) {
final String outputDir = erlProject.getProperties().getOutputDir()
.toString();
final RpcFuture f = helper.startCompileErl(project, bres, outputDir,
backend.getOtpRpc(), compilerOptions, kind == BuildKind.FULL);
if (f != null) {
results.put(f, resource);
}
} else if ("yrl".equals(resource.getFileExtension())) {
final RpcFuture f = helper.startCompileYrl(project, resource,
backend.getOtpRpc(), compilerOptions);
if (f != null) {
results.put(f, resource);
}
} else {
ErlLogger.warn("Don't know how to compile: %s", resource.getName());
}
}
final List<Entry<RpcFuture, IResource>> done = Lists.newArrayList();
final List<Entry<RpcFuture, IResource>> waiting = Lists
.newArrayList(results.entrySet());
// TODO should use some kind of notification!
while (!waiting.isEmpty()) {
for (final Entry<RpcFuture, IResource> result : waiting) {
notifier.checkCancel();
OtpErlangObject r;
try {
r = result.getKey().get(100, TimeUnit.MILLISECONDS);
} catch (final Exception e) {
r = null;
}
if (r != null) {
final IResource resource = result.getValue();
helper.completeCompile(project, resource, r, backend.getOtpRpc(),
compilerOptions);
notifier.compiled(resource);
done.add(result);
}
}
waiting.removeAll(done);
done.clear();
}
helper.refreshOutputDir(project);
try {
helper.checkForClashes(backend.getOtpRpc(), project);
} catch (final Exception e) {
}
backend.removeProjectPath(model.findProject(project));
}
private void handleAppFile(final IProject project, final String outPath,
final Collection<IPath> sources) {
if (SystemConfiguration.hasFeatureEnabled("erlide.no_app_src")) {
return;
}
// if project doesn't look like an OTP app, skip this step
if (!sources.contains(new Path("src"))) {
return;
}
// ignore other dirs than 'src'
final IPath src = new Path("src");
final IFolder dir = (IFolder) project.findMember(src);
if (dir != null) {
try {
for (final IResource file : dir.members()) {
final String name = file.getName();
if (name.endsWith(".app.src")) {
final String appSrc = file.getLocation().toPortableString();
final String destPath = outPath + "/"
+ name.substring(0, name.lastIndexOf('.'));
final Collection<String> modules = gatherModules(project);
fillAppFileDetails(project, appSrc, destPath, modules);
}
}
} catch (final CoreException e) {
ErlLogger.error(e);
}
}
}
private Collection<String> gatherModules(final IProject project) {
final Collection<String> modules = newArrayList();
try {
final IErlProject erlangProject = ErlangEngine.getInstance().getModel()
.getErlangProject(project);
for (final IErlModule m : erlangProject.getModules()) {
// ignore rebar deps;
if (!ignoreModule(erlangProject, m)) {
modules.add(m.getModuleName());
}
}
} catch (final ErlModelException e1) {
ErlLogger.error(e1);
}
return modules;
}
private boolean ignoreModule(final IErlProject erlangProject, final IErlModule m) {
boolean result = false;
result |= m.getSourceKind() != SourceKind.ERL;
result |= !isModuleOnDirectSourcePath(erlangProject, m);
result |= m.getResource().getProjectRelativePath().segment(0).equals("deps");
if (result) {
ErlLogger.debug(".app: ignore " + m.getResource().getProjectRelativePath());
}
return result;
}
private boolean isModuleOnDirectSourcePath(final IErlProject erlangProject,
final IErlModule m) {
boolean result = false;
final List<IPath> sourceDirs = Lists
.newArrayList(erlangProject.getProperties().getSourceDirs());
for (final IPath p : sourceDirs) {
if (m.getResource().getParent().getProjectRelativePath().equals(p)) {
result = true;
break;
}
}
return result;
}
private void fillAppFileDetails(final IProject project, final String appSrc,
final String destPath, final Collection<String> modules) {
try {
final IErlProject eproject = ErlangEngine.getInstance().getModel()
.findProject(project);
if (eproject == null) {
return;
}
final IBackend backend = BackendCore.getBackendManager()
.getBuildBackend(eproject);
backend.getOtpRpc().call("erlide_builder", "compile_app_src", "ssla", appSrc,
destPath, modules);
} catch (final Exception e) {
ErlLogger.error(e);
}
}
private void initializeBuilder(final BuildNotifier notifier) {
notifier.begin();
}
private void cleanup(final BuildNotifier notifier) {
notifier.done();
}
private Set<BuildResource> getResourcesToBuild(final BuildKind kind,
final IProject currentProject, final IResourceDelta myDelta,
final BuildNotifier notifier) throws CoreException {
Set<BuildResource> resourcesToBuild = Sets.newHashSet();
notifier.beginTask("retrieving resources to build", IProgressMonitor.UNKNOWN);
if (kind == BuildKind.FULL) {
resourcesToBuild = helper.getAffectedResources(currentProject, notifier);
} else {
final Path path = new Path(".settings/org.erlide.core.prefs");
if (myDelta != null && myDelta.findMember(path) != null) {
ErlLogger.info("project configuration changed: doing full rebuild");
resourcesToBuild = helper.getAffectedResources(currentProject, notifier);
} else {
resourcesToBuild = helper.getAffectedResources(myDelta, notifier);
}
}
notifier.doneTask();
return resourcesToBuild;
}
public IResource findCorrespondingSource(final IResource beam) throws CoreException {
final String[] p = beam.getName().split("\\.");
final SearchVisitor searcher = helper.new SearchVisitor(p[0], null);
beam.getProject().accept(searcher);
final IResource source = searcher.getResult();
return source;
}
@Override
public BuilderProperties getProperties() {
return null;
}
}