/** * Aptana Studio * Copyright (c) 2005-2012 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions). * Please see the license.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.ruby.launching; import java.io.File; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.contentobjects.jnotify.IJNotify; import net.contentobjects.jnotify.JNotifyException; import net.contentobjects.jnotify.JNotifyListener; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Plugin; import org.osgi.framework.BundleContext; import com.aptana.core.ShellExecutable; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.ExecutableUtil; import com.aptana.core.util.PlatformUtil; import com.aptana.core.util.ProcessUtil; import com.aptana.filewatcher.FileWatcher; public class RubyLaunchingPlugin extends Plugin { public static final String PLUGIN_ID = "com.aptana.ruby.launching"; //$NON-NLS-1$ private static final String GEM_COMMAND = "gem"; //$NON-NLS-1$ private static final String RUBYW = "rubyw"; //$NON-NLS-1$ private static final String RBENV = "rbenv"; //$NON-NLS-1$ public static final String RUBY = "ruby"; //$NON-NLS-1$ public static final String RAKE = "rake"; //$NON-NLS-1$ public static final String RAKEFILE = "Rakefile"; //$NON-NLS-1$ /** * Map from project to ruby version. FIXME make use of the workingDirToRubyExe map? */ private static Map<IProject, String> projectToVersion = new HashMap<IProject, String>(); /** * map of working directories to the corresponding Ruby interpreter found on PATH there. */ private static Map<IPath, IPath> workingDirToRubyExe = new HashMap<IPath, IPath>(); /** * Map from ruby interpreter to loadpaths */ private static Map<String, Set<IPath>> rubyToLoadPaths = new HashMap<String, Set<IPath>>(); /** * Map from ruby interpreter to set of gem paths. FIXME I'm not sure this is 100% RVM friendly since they could use * gemsets... */ private static Map<String, Set<IPath>> rubyToGemPaths = new HashMap<String, Set<IPath>>(); /** * Cache from ruby interpreter path to version string */ private static Map<IPath, String> pathToVersion = new HashMap<IPath, String>(); protected static RubyLaunchingPlugin plugin; private static RbenvVersionListener rbEnvVersionListener; private static Map<IPath, Integer> filewatcherIds = new HashMap<IPath, Integer>(); public static IPath getRakePath(IPath workingDir) { return getBinaryScriptPath(RAKE, workingDir); } public static IPath resolveRBENVShimPath(IPath rbenvShimPath, IPath workingDir) { if (rbenvShimPath == null) { return null; } // if we're using rbenv, resolve to the underlying install/script we're targeting. // we can't chain the rbenv shims together on the command line or it all blows up. if (!Platform.OS_WIN32.equals(Platform.getOS()) && rbenvShimPath.toOSString().contains("/.rbenv/")) //$NON-NLS-1$ { IPath rbEnvPath = ExecutableUtil.find(RBENV, false, null, workingDir); if (rbEnvPath != null) { IStatus status = ProcessUtil.runInBackground(rbEnvPath.toOSString(), workingDir, "which", rbenvShimPath.lastSegment()); //$NON-NLS-1$ if (status.isOK()) { return Path.fromOSString(status.getMessage()); } } } return rbenvShimPath; } /** * Search for the applicable ruby executable for the working dir. If no working dir is set, we won't take rvmrc into * account (we'll use global PATH). * * @param workingDir * @return */ public static IPath rubyExecutablePath(IPath workingDir) { // Use Path.ROOT in place of null working dir IPath pathKey = (workingDir == null ? Path.ROOT : workingDir); if (!workingDirToRubyExe.containsKey(pathKey)) { IPath path = null; if (Platform.OS_WIN32.equals(Platform.getOS())) { path = ExecutableUtil.find(RUBYW, true, getCommonRubyBinaryLocations(RUBYW), workingDir); } if (path == null) { path = ExecutableUtil.find(RUBY, true, getCommonRubyBinaryLocations(RUBY), workingDir); IPath resolved = resolveRBENVShimPath(path, workingDir); if (resolved != null) { if (workingDir != null && !resolved.equals(path)) { // We had to dereference an rbenv shim. We need to hook up some listener here to handle // .rbenv-version file changes try { if (rbEnvVersionListener == null) { rbEnvVersionListener = new RbenvVersionListener(); } Integer watchId = filewatcherIds.get(pathKey); if (watchId == null) { watchId = FileWatcher.addWatch(workingDir.toOSString(), IJNotify.FILE_ANY, false, rbEnvVersionListener); filewatcherIds.put(pathKey, watchId); } } catch (JNotifyException e) { IdeLog.logError(getDefault(), e); } } path = resolved; } } if (IdeLog.isInfoEnabled(getDefault(), IDebugScopes.DEBUG)) { IdeLog.logInfo(getDefault(), MessageFormat.format("Found ruby executable ''{0}'' for working directory: {1}", path, pathKey)); //$NON-NLS-1$ } workingDirToRubyExe.put(pathKey, path); } return workingDirToRubyExe.get(pathKey); } /** * Return an ordered list of common locations that you'd find a ruby binary. * * @return */ private static List<IPath> getCommonRubyBinaryLocations(String binaryName) { List<IPath> locations = new ArrayList<IPath>(); if (Platform.OS_WIN32.equals(Platform.getOS())) { locations.add(Path.fromOSString("C:\\ruby\\bin").append(binaryName).addFileExtension("exe")); //$NON-NLS-1$ //$NON-NLS-2$ } else { locations.add(Path.fromOSString(PlatformUtil.expandEnvironmentStrings("~/.rvm/bin/" + binaryName))); //$NON-NLS-1$ locations.add(Path.fromOSString("/opt/local/bin/").append(binaryName)); //$NON-NLS-1$ locations.add(Path.fromOSString("/usr/local/bin/").append(binaryName)); //$NON-NLS-1$ locations.add(Path.fromOSString("/usr/bin/").append(binaryName)); //$NON-NLS-1$ } if (Platform.OS_MACOSX.equals(Platform.getOS())) { locations.add(Path.fromOSString("/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/") //$NON-NLS-1$ .append(binaryName)); } return locations; } /** * Return the version string for the ruby interpreter set up for a given project. * * @param project * @return */ public static synchronized String getRubyVersionForProject(IProject project) { if (project == null) { return null; } // This seems expensive, so we're caching the version per-project if (projectToVersion.containsKey(project)) { return projectToVersion.get(project); } IPath rubyExe = rubyExecutablePath(project.getLocation()); String version = getRubyVersion(rubyExe); projectToVersion.put(project, version); return version; } public synchronized static Set<IPath> getLoadpaths(IProject project) { IPath workingDir = (project == null ? null : project.getLocation()); IPath rubyPath = rubyExecutablePath(workingDir); String rubyExe = (rubyPath == null ? RUBY : rubyPath.toOSString()); if (!rubyToLoadPaths.containsKey(rubyExe)) { Map<String, String> env = null; if (!Platform.OS_WIN32.equals(Platform.getOS())) { env = ShellExecutable.getEnvironment(workingDir); } String rawLoadPathOutput = ProcessUtil.outputForCommand(rubyExe, null, env, "-e", "puts $:"); //$NON-NLS-1$ //$NON-NLS-2$ if (rawLoadPathOutput == null) { rubyToLoadPaths.put(rubyExe, null); } else { Set<IPath> paths = new HashSet<IPath>(); String[] loadpaths = rawLoadPathOutput.split("\r\n|\r|\n"); //$NON-NLS-1$ if (loadpaths != null) { for (String loadpath : loadpaths) { if (".".equals(loadpath)) //$NON-NLS-1$ { continue; } paths.add(new Path(loadpath)); } } rubyToLoadPaths.put(rubyExe, paths); } } Set<IPath> result = rubyToLoadPaths.get(rubyExe); if (result == null) { return Collections.emptySet(); } return result; } /** * Handles resolving RBENV shims * * @param binary * @param pathsToSearch * @param workingDirectory * @return */ public static IPath getBinaryScriptPath(String binary, List<IPath> pathsToSearch, IPath workingDirectory) { IPath path = ExecutableUtil.find(binary, false, pathsToSearch, workingDirectory); if (Platform.OS_WIN32.equals(Platform.getOS())) { // No RBENV on Windows! return path; } return resolveRBENVShimPath(path, workingDirectory); } /** * Handles resolving RBENV shims * * @param binary * @param workingDirectory * @return */ public static IPath getBinaryScriptPath(String binary, IPath workingDirectory) { return getBinaryScriptPath(binary, null, workingDirectory); } public synchronized static Set<IPath> getGemPaths(IProject project) { // FIXME this is including every single gem! We should narrow the list down based on Gemfile in project root if // we can! IPath wd = (project == null ? null : project.getLocation()); IPath rubyPath = rubyExecutablePath(wd); String rubyPathString = rubyPath == null ? RUBY : rubyPath.toOSString(); if (!rubyToGemPaths.containsKey(rubyPathString)) { IPath gemBinPath = getBinaryScriptPath(GEM_COMMAND, wd); String gemCommand = gemBinPath == null ? GEM_COMMAND : gemBinPath.toOSString(); // FIXME Will this actually behave properly with RVM? // FIXME Not finding my user gem path on Windows... Map<String, String> env = null; if (!Platform.OS_WIN32.equals(Platform.getOS())) { env = ShellExecutable.getEnvironment(wd); } IStatus status = ProcessUtil.runInBackground(rubyPathString, wd, env, gemCommand, "env", "gempath"); //$NON-NLS-1$ //$NON-NLS-2$ String gemEnvOutput = null; if (status.isOK()) { gemEnvOutput = status.getMessage(); } if (gemEnvOutput == null) { rubyToGemPaths.put(rubyPathString, null); } else { Set<IPath> paths = new HashSet<IPath>(); String[] gemPaths = gemEnvOutput.split(File.pathSeparator); if (gemPaths != null) { for (String gemPath : gemPaths) { IPath gemsPath = new Path(gemPath).append("gems"); //$NON-NLS-1$ paths.add(gemsPath); } } rubyToGemPaths.put(rubyPathString, paths); } } Set<IPath> result = rubyToGemPaths.get(rubyPathString); if (result == null) { return Collections.emptySet(); } return result; } public RubyLaunchingPlugin() { super(); } public static Plugin getDefault() { return plugin; } /* * (non-Javadoc) * @see org.eclipse.core.runtime.Plugin#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { super.start(context); plugin = this; } @Override public void stop(BundleContext context) throws Exception { try { for (Map.Entry<IPath, Integer> entry : filewatcherIds.entrySet()) { try { FileWatcher.removeWatch(entry.getValue()); } catch (Exception e) { IdeLog.logError(getDefault(), e); } } } finally { filewatcherIds = null; plugin = null; projectToVersion = null; workingDirToRubyExe = null; rubyToLoadPaths = null; pathToVersion = null; super.stop(context); } } public static String getPluginIdentifier() { return PLUGIN_ID; } public synchronized static String getRubyVersion(IPath rubyExe) { if (!pathToVersion.containsKey(rubyExe)) { String rubyPath = (rubyExe == null ? RUBY : rubyExe.toOSString()); Map<String, String> env = null; if (!Platform.OS_WIN32.equals(Platform.getOS())) { env = ShellExecutable.getEnvironment(); } String version = ProcessUtil.outputForCommand(rubyPath, null, env, "-v"); //$NON-NLS-1$ pathToVersion.put(rubyExe, version); } return pathToVersion.get(rubyExe); } /** * Listens for rbenv version changes to projects. When the special file changes, we wipe our in-memory cache of ruby * executable/version so we re-calculate the next time we need them. * * @author cwilliams */ private static class RbenvVersionListener implements JNotifyListener { private static final String RBENV_VERSION_FILENAME = ".rbenv-version"; //$NON-NLS-1$ public void fileRenamed(int wd, String rootPath, String oldName, String newName) { if (newName.equals(RBENV_VERSION_FILENAME)) { fileCreated(wd, rootPath, newName); } else if (oldName.equals(RBENV_VERSION_FILENAME)) { fileDeleted(wd, rootPath, oldName); } } public void fileModified(int wd, String rootPath, String name) { if (name.equals(RBENV_VERSION_FILENAME)) { // Wipe out the cached version since it's been changed IPath path = Path.fromOSString(rootPath); workingDirToRubyExe.remove(path); pathToVersion.remove(path); } } public void fileDeleted(int wd, String rootPath, String name) { fileModified(wd, rootPath, name); } public void fileCreated(int wd, String rootPath, String name) { fileModified(wd, rootPath, name); } } }