/* * Copyright (c) 2012, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.core.internal.builder; import com.google.common.base.Charsets; import com.google.common.io.Files; import com.google.dart.engine.sdk.DirectoryBasedDartSdk; import com.google.dart.tools.core.DartCore; import com.google.dart.tools.core.builder.AbstractBuildVisitor; import com.google.dart.tools.core.builder.BuildEvent; import com.google.dart.tools.core.builder.BuildParticipant; import com.google.dart.tools.core.builder.CleanEvent; import com.google.dart.tools.core.builder.CleanVisitor; import com.google.dart.tools.core.dart2js.ProcessRunner; import com.google.dart.tools.core.model.DartSdkManager; import com.google.dart.tools.core.pub.IPackageRootProvider; import com.google.dart.tools.core.snapshot.SnapshotCompilationServer; import static com.google.dart.tools.core.DartCore.BUILD_DART_FILE_NAME; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; 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.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.IResourceProxy; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; /** * This class invokes the build.dart scripts in a project's root directory. * <p> * For full builds, --full is passed into the dart script. For clean builds, we pass in a --clean * flag. For all other builds, we passed in the list of changed files using --changed and --removed * parameters. E.g. --changed=file1.txt --changed=file2.foo --removed=file3.bar. * * @see DartBuilder * @coverage dart.tools.core.builder */ public class BuildDartParticipant implements BuildParticipant { // The name of the build.dart snapshot file. private static final String BUILD_DART_SNAPSHOT_NAME = "build.snapshot"; // The generic unix/max/bsd CLI limit is 262144. private static final int GENERAL_CLI_LIMIT = 262000; // The win32 CreateProcess() function can take a max of 32767 chars. private static final int WIN_CLI_LIMIT = 32500; static final String ISSUE_MARKER = DartCore.PLUGIN_ID + ".buildDartIssue"; private static final String CLEAN = "--clean"; private static final String FULL_BUILD = "--full"; private static final String MACHINE = "--machine"; private static final String BUILD_LOG_NAME = ".buildlog"; private static boolean USE_SNAPSHOT = false; private static void createErrorMarker(IFile file, int severity, String message, int line, int charStart, int charEnd) throws CoreException { IMarker marker = file.createMarker(ISSUE_MARKER); marker.setAttribute(IMarker.SEVERITY, severity); marker.setAttribute(IMarker.MESSAGE, message); marker.setAttribute(IMarker.LINE_NUMBER, line); if (charStart != -1) { marker.setAttribute(IMarker.CHAR_START, charStart); } if (charEnd != -1) { marker.setAttribute(IMarker.CHAR_END, charEnd); } } private static void deleteMarkers(IContainer container) throws CoreException { container.deleteMarkers(ISSUE_MARKER, true, IResource.DEPTH_INFINITE); } private static void deleteMarkers(IFile file) throws CoreException { file.deleteMarkers(ISSUE_MARKER, true, IResource.DEPTH_ZERO); } private IPackageRootProvider packageRootProvider; private String genDirPath; public BuildDartParticipant() { this(IPackageRootProvider.DEFAULT); } public BuildDartParticipant(IPackageRootProvider packageRootProvider) { this.packageRootProvider = packageRootProvider; } /** * Traverse resources looking for build.dart files that has been added or which has resources * contained in its parent which have been added or have changed. */ @Override public void build(BuildEvent event, IProgressMonitor monitor) throws CoreException { if (!shouldRunAnyBuildDart(event.getProject())) { return; } if (event.isFullBuild()) { event.traverse(new AbstractBuildVisitor() { @Override public boolean visit(IResourceProxy proxy, IProgressMonitor monitor) throws CoreException { if (proxy.getType() == IResource.FOLDER || proxy.getType() == IResource.PROJECT) { IContainer container = (IContainer) proxy.requestResource(); IFile builderFile = container.getFile(new Path(BUILD_DART_FILE_NAME)); if (DartCore.isBuildDart(builderFile)) { // Perform a full build. runBuildDart(builderFile, Arrays.asList(FULL_BUILD), monitor); return false; } } return true; } }, false); } else { // Perform an incremental build. event.traverse(new AbstractBuildVisitor() { @Override public boolean visit(IResourceDelta delta, IProgressMonitor monitor) throws CoreException { IResource resource = delta.getResource(); if (resource.getType() == IResource.FOLDER || resource.getType() == IResource.PROJECT) { IFile builderFile = ((IContainer) resource).getFile(new Path(BUILD_DART_FILE_NAME)); if (DartCore.isBuildDart(builderFile)) { processDelta(builderFile, 0, delta, monitor); return false; } } return true; } }, false); } } /** * Clear markers and invoke each builder with the --clean flag */ @Override public void clean(CleanEvent event, final IProgressMonitor monitor) throws CoreException { deleteMarkers(event.getProject()); DartCore.clearResourceRemapping(event.getProject()); event.traverse(new CleanVisitor() { @Override public boolean visit(IResourceProxy proxy, IProgressMonitor monitor) throws CoreException { if (proxy.getType() == IResource.FILE) { if (proxy.getName().equals(BUILD_DART_FILE_NAME)) { IFile builderFile = (IFile) proxy.requestResource(); if (DartCore.isBuildDart(builderFile)) { runBuildDart(builderFile, Arrays.asList(new String[] {CLEAN}), monitor); } } } return true; } }, false); } /** * Process the specified delta and invoke the builder as appropriate. * * @param builderFile the build.dart file (not <code>null</code>) * @param delta the resource delta or <code>null</code> if none * @param monitor the progress monitor (not <code>null</code>) to use for reporting progress to * the user. It is the caller's responsibility to call done() on the given monitor. */ protected void processDelta(IFile builderFile, int kind, IResourceDelta delta, IProgressMonitor monitor) throws CoreException { // Find the changed and removed files. List<IFile> changedFiles = new ArrayList<IFile>(); List<IFile> deletedFiles = new ArrayList<IFile>(); getFileDeltas(delta, changedFiles, deletedFiles); // Construct the args array. int containerDepth = builderFile.getParent().getFullPath().segmentCount(); List<String> args = new ArrayList<String>(changedFiles.size() + deletedFiles.size()); for (IFile file : changedFiles) { deleteMarkers(file); DartCore.clearResourceRemapping(file); args.add("--changed=" + getFilePath(containerDepth, file)); } for (IFile file : deletedFiles) { DartCore.clearResourceRemapping(file); args.add("--removed=" + getFilePath(containerDepth, file)); } // Only invoke builder if there were changes of interest if (args.size() > 0) { runBuildDart(builderFile, args, monitor); } } /** * Execute the build.dart application. This method is overridden during testing to record which * build.dart files would be run rather than actually running them. * * @param builderFile the build.dart file (not <code>null</code>) * @param buildArgs the arguments passed to the build file * @param monitor the progress monitor (not <code>null</code>) to use for reporting progress to * the user. It is the caller's responsibility to call done() on the given monitor. */ protected void runBuildDart(IFile builderFile, List<String> buildArgs, IProgressMonitor monitor) throws CoreException { StringBuilder msg = new StringBuilder(); msg.append("Running "); msg.append(builderFile.getLocation()); monitor.beginTask(msg.toString(), IProgressMonitor.UNKNOWN); IContainer container = builderFile.getParent(); buildArgs = new ArrayList<String>(buildArgs); buildArgs.add(0, MACHINE); String commandSummary = createCommandSummary(buildArgs); // If we're over the CLI length limit, instead of sending in a comprehensive list of all the // changes and deletions, just request a full build. if ((DartCore.isWindows() && commandSummary.length() > WIN_CLI_LIMIT) || (!DartCore.isWindows() && commandSummary.length() > GENERAL_CLI_LIMIT)) { buildArgs.clear(); buildArgs.add(MACHINE); buildArgs.add(FULL_BUILD); commandSummary = createCommandSummary(buildArgs); } // Trim long command summaries - used for verbose printing. if (commandSummary.length() > 100) { commandSummary = commandSummary.substring(0, 100) + "..."; } ProcessBuilder builder = new ProcessBuilder(); List<String> args = new ArrayList<String>(); SnapshotCompilationServer snapshotCompiler = null; IStatus snapshotStatus = Status.OK_STATUS; long startTime = System.currentTimeMillis(); if (USE_SNAPSHOT) { snapshotCompiler = new SnapshotCompilationServer(builderFile.getLocation().toFile()); snapshotStatus = snapshotCompiler.compile(); if (!snapshotStatus.isOK()) { snapshotCompiler = null; DartCore.logError(snapshotStatus.toString()); } } args.add(DartSdkManager.getManager().getSdk().getVmExecutable().getPath()); // --package-root File packageRoot = packageRootProvider.getPackageRoot(builderFile.getProject()); if (packageRoot != null) { String path = packageRoot.getPath(); if (!path.endsWith(File.separator)) { path += File.separator; } args.add("--package-root=" + path); } // If we have a snapshot, use that. if (snapshotCompiler != null && snapshotCompiler.getDestFile().exists()) { args.add(snapshotCompiler.getDestFile().getPath()); } else { args.add(builderFile.getName()); } args.addAll(buildArgs); //TODO (danrubel): Older build.dart may rely on DART_SDK env var... so leave for now Map<String, String> env = builder.environment(); DirectoryBasedDartSdk sdk = DartSdkManager.getManager().getSdk(); env.put("DART_SDK", sdk.getDirectory().getAbsolutePath()); builder.command(args); builder.directory(container.getLocation().toFile()); builder.redirectErrorStream(true); ProcessRunner runner = new ProcessRunner(builder); logStart(builderFile); log(builderFile, "---\nbuild.dart " + commandSummary); int result; try { // The monitor argument is just used to listen for user cancellations. result = runner.runSync(monitor); } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, DartCore.PLUGIN_ID, e.getMessage(), e)); } String output = runner.getStdOut(); String processedOutput = processBuilderOutput(container, output); log(builderFile, output.trim()); if (result != 0) { DartCore.getConsole().printSeparator("build.dart " + commandSummary); if (builderFile.getLocationURI() != null) { DartCore.getConsole().println(builderFile.getLocationURI().toString()); } DartCore.getConsole().println("build.dart returned error code " + result); String stdout = processedOutput.trim(); if (stdout.length() > 0) { DartCore.getConsole().println(); DartCore.getConsole().println(stdout); } DartCore.getConsole().println(); } long elapsedTime = System.currentTimeMillis() - startTime; log(builderFile, "build.dart finished [" + elapsedTime + " ms]\n"); BuilderUtil.delayedRefresh(builderFile.getParent()); } private String createCommandSummary(List<String> buildArgs) { StringBuilder builder = new StringBuilder(); for (String arg : buildArgs) { builder.append(arg); builder.append(' '); } return builder.toString().trim(); } private void createMarker(IContainer container, int severity, String path, String message, int line, int charStart, int charEnd) { IFile file = container.getFile(new Path(path)); if (file.exists()) { try { createErrorMarker(file, severity, message, line, charStart, charEnd); } catch (CoreException e) { DartCore.logError(e); } } } private void getFileDeltas(IResourceDelta delta, final List<IFile> changedFiles, final List<IFile> deletedFiles) throws CoreException { delta.accept(new IResourceDeltaVisitor() { @Override public boolean visit(IResourceDelta delta) throws CoreException { IResource resource = delta.getResource(); if (resource.getType() == IResource.FILE) { // Don't report changes to "." files. if (resource.getName().startsWith(".")) { return false; } // Don't report changes to "build.dart" files if (resource.getName().equals(BUILD_DART_FILE_NAME)) { return false; } // Don't report changes to "build.snapshot" files if (resource.getName().equals(BUILD_DART_SNAPSHOT_NAME)) { return false; } switch (delta.getKind()) { case IResourceDelta.ADDED: changedFiles.add((IFile) resource); break; case IResourceDelta.CHANGED: changedFiles.add((IFile) resource); break; case IResourceDelta.REMOVED: deletedFiles.add((IFile) resource); break; } } else if (resource.getType() == IResource.FOLDER) { // Don't report changes in hidden directories, specifically SCM (.svn, .git) directories. // https://code.google.com/p/dart/issues/detail?id=4885 if (resource.getName().startsWith(".")) { return false; } // This is to address dartbug.com/10863. // A more complete fix will happen for dartbug.com/8478. if (resource.getName().equals("out")) { return false; } // Don't trigger builds from packages directories. if (resource.getName().equals(DartCore.PACKAGES_DIRECTORY_NAME)) { return false; } } return true; } }); } /** * Answer the path of the specified file relative to a container * * @param containerDepth the number of segments in the containers full path * @param file the file (not <code>null</code>) */ private String getFilePath(int containerDepth, IFile file) { return file.getFullPath().removeFirstSegments(containerDepth).toOSString(); } private void handleBuilderMessage(IContainer container, JSONObject json) throws JSONException { String method = json.getString("method"); JSONObject params = json.optJSONObject("params"); if (method.equals("error")) { createMarker( container, IMarker.SEVERITY_ERROR, params.getString("file"), params.getString("message"), params.optInt("line", -1), params.optInt("charStart", -1), params.optInt("charEnd", -1)); } else if (method.equals("warning")) { createMarker( container, IMarker.SEVERITY_WARNING, params.getString("file"), params.getString("message"), params.optInt("line", -1), params.optInt("charStart", -1), params.optInt("charEnd", -1)); } else if (method.equals("info")) { String file = params.optString("file", null); if (file != null && file.length() > 0) { createMarker( container, IMarker.SEVERITY_INFO, file, params.getString("message"), params.optInt("line", -1), params.optInt("charStart", -1), params.optInt("charEnd", -1)); } else { //[{"method":"info","params":{"message":"Took 0.6s (0.3s awaiting secondary inputs)."}}] } } else if (method.equals("mapping")) { String fromPath = params.getString("from"); String toPath = params.getString("to"); IFile fromFile = container.getFile(new Path(fromPath)); IFile toFile = container.getFile(new Path(toPath)); if (fromFile.exists()) { DartCore.setResourceRemapping(fromFile, toFile); } } else if (method.equals("out")) { genDirPath = params.getString("file"); } else if (method.equals("generated")) { // TODO(keertip): add processing for generated messages } else { DartCore.logError("builder command '" + method + "\' not understood."); } } private void log(IFile builderFile, String string) { try { IFile logFile = builderFile.getParent().getFile(new Path(BUILD_LOG_NAME)); File file = new File(logFile.getLocationURI().toURL().toURI()); Files.append(string + "\n", file, Charsets.UTF_8); } catch (IOException ioe) { } catch (URISyntaxException e) { } } private void logStart(IFile builderFile) { final long TRUNC_SIZE = 1024 * 1024; try { IFile logFile = builderFile.getParent().getFile(new Path(BUILD_LOG_NAME)); File file = new File(logFile.getLocationURI().toURL().toURI()); if (logFile.exists()) { if (file.length() > TRUNC_SIZE) { RandomAccessFile rFile = new RandomAccessFile(file, "rw"); rFile.setLength(0); rFile.close(); } } else { Files.touch(file); } } catch (IOException ioe) { } catch (URISyntaxException e) { } } private String processBuilderOutput(IContainer container, String output) { String[] lines = output.split("\n"); StringBuilder stringBuilder = new StringBuilder(output.length()); for (String line : lines) { String trimmedLine = line.trim(); if (trimmedLine.startsWith("[{") && trimmedLine.endsWith("}]")) { // try and parse this as a builder event //[{"method":"error","params":{"file":"foo.html","line":23,"message":"no ID found"}}] //[{"method":"warning","params":{"file":"foo.html","line":23,"message":"no ID found"}}] //[{"method":"info","params":{"file":"foo.html","line":23,"message":"no ID found"}}] //[{"method":"info","params":{"message":"Took 0.6s (0.3s awaiting secondary inputs)."}}] //[{"method":"mapping","params":{"from":"foo.html","to":"out/foo.html"}}] String jsonStr = trimmedLine.substring(1, trimmedLine.length() - 1); try { JSONObject json = new JSONObject(jsonStr); handleBuilderMessage(container, json); } catch (JSONException e) { DartCore.logError("Failed to process build.dart message:\n" + trimmedLine, e); } } else { stringBuilder.append(line); stringBuilder.append('\n'); } } return stringBuilder.toString(); } /** * @return whether we should invoke any build.dart files in the given project */ private boolean shouldRunAnyBuildDart(IProject project) { boolean disableBuilder = DartCore.getPlugin().getDisableDartBasedBuilder(project); return !disableBuilder; } }