/* * Copyright 2015-present Facebook, Inc. * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.facebook.buck.intellij.ideabuck.util; import static java.io.File.pathSeparator; import com.facebook.buck.intellij.ideabuck.environment.EnvironmentFilter; import com.facebook.buck.intellij.ideabuck.environment.Platform; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.intellij.openapi.diagnostic.Logger; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.stream.StreamSupport; /** * Given the name of an executable, search a set of (possibly platform-specific) known locations for * that executable. */ public class ExecutableFinder { private static final Logger LOG = Logger.getInstance(ExecutableFinder.class); private static final ImmutableSet<String> DEFAULT_WINDOWS_EXTENSIONS = ImmutableSet.of( ".bat", ".cmd", ".com", ".cpl", ".exe", ".js", ".jse", ".msc", ".vbs", ".wsf", ".wsh"); private final Platform platform; public ExecutableFinder() { this(Platform.detect()); } @VisibleForTesting public ExecutableFinder(Platform platform) { this.platform = platform; } public Path getExecutable(Path suggestedExecutable, ImmutableMap<String, String> env) { Optional<Path> exe = getOptionalExecutable(suggestedExecutable, env); if (!exe.isPresent()) { throw new RuntimeException( String.format( "Unable to locate %s on PATH, or it's not marked as being executable", suggestedExecutable)); } return exe.get(); } public Optional<Path> getOptionalExecutable( Path suggestedExecutable, ImmutableMap<String, String> env) { env = EnvironmentFilter.filteredEnvironment(env, platform); return getOptionalExecutable(suggestedExecutable, getPaths(env), getExecutableSuffixes(env)); } public Optional<Path> getOptionalExecutable( Path suggestedExecutable, ImmutableCollection<Path> path, ImmutableCollection<String> fileSuffixes) { // Fast path out of here. if (isExecutable(suggestedExecutable)) { return Optional.of(suggestedExecutable); } Optional<Path> executable = FileFinder.getOptionalFile( FileFinder.combine( /* prefixes */ null, suggestedExecutable.toString(), ImmutableSet.copyOf(fileSuffixes)), path, ExecutableFinder::isExecutable); LOG.debug("Executable '%s' mapped to '%s'", suggestedExecutable, executable); return executable; } private static boolean isExecutable(Path exe) { if (!Files.exists(exe)) { return false; } if (Files.isSymbolicLink(exe)) { try { Path target = Files.readSymbolicLink(exe); return isExecutable(exe.resolveSibling(target).normalize()); } catch (IOException | SecurityException e) { // NOPMD } } if (Files.isDirectory(exe)) { LOG.debug("Found potential executable, but is a directory: %s", exe); return false; } if (!Files.isExecutable(exe) && !Files.isSymbolicLink(exe)) { LOG.debug("Found potential executable, but not actually executable: %s", exe); return false; } return true; } private ImmutableSet<Path> getPaths(ImmutableMap<String, String> env) { ImmutableSet.Builder<Path> paths = ImmutableSet.builder(); // Add the empty path so that when we iterate over it, we can check for the suffixed version of // a given path, be it absolute or not. paths.add(Paths.get("")); String pathEnv = env.get("PATH"); if (pathEnv != null) { pathEnv = pathEnv.trim(); paths.addAll( StreamSupport.stream( Splitter.on(pathSeparator).omitEmptyStrings().split(pathEnv).spliterator(), false) .map(Paths::get) .iterator()); } if (platform == Platform.MACOS) { Path osXPaths = Paths.get("/etc/paths"); if (Files.exists(osXPaths)) { try { paths.addAll( Files.readAllLines(osXPaths, Charset.defaultCharset()) .stream() .map(Paths::get) .iterator()); } catch (IOException e) { LOG.warn("Unable to read mac-specific paths. Skipping"); } } } return paths.build(); } private ImmutableSet<String> getExecutableSuffixes(ImmutableMap<String, String> env) { if (platform == Platform.WINDOWS) { String pathext = env.get("PATHEXT"); if (pathext == null) { return DEFAULT_WINDOWS_EXTENSIONS; } return ImmutableSet.<String>builder() .addAll(Splitter.on(";").omitEmptyStrings().split(pathext)) .build(); } return ImmutableSet.of(""); } }