/* * Copyright 2017 The Bazel Authors. All rights reserved. * * 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.google.idea.blaze.python.run; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.idea.blaze.base.command.BlazeCommandName; import com.google.idea.blaze.base.command.BlazeFlags; import com.google.idea.blaze.base.command.BlazeInvocationContext; import com.google.idea.blaze.base.command.buildresult.BlazeArtifact; import com.google.idea.blaze.base.command.buildresult.BuildResultHelper; import com.google.idea.blaze.base.command.buildresult.BuildResultHelper.GetArtifactsException; import com.google.idea.blaze.base.command.buildresult.BuildResultHelperProvider; import com.google.idea.blaze.base.io.FileOperationProvider; import com.google.idea.blaze.base.model.BlazeProjectData; import com.google.idea.blaze.base.model.primitives.Label; import com.google.idea.blaze.base.model.primitives.TargetExpression; import com.google.idea.blaze.base.model.primitives.WorkspaceRoot; import com.google.idea.blaze.base.run.BlazeBeforeRunCommandHelper; import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration; import com.google.idea.blaze.base.run.ExecutorType; import com.google.idea.blaze.base.run.WithBrowserHyperlinkExecutionException; import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationRunner.BlazeCommandRunProfileState; import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner; import com.google.idea.blaze.base.run.filter.BlazeTargetFilter; import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState; import com.google.idea.blaze.base.sync.aspects.BuildResult; import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager; import com.google.idea.blaze.base.util.ProcessGroupUtil; import com.google.idea.blaze.base.util.SaveUtil; import com.google.idea.blaze.python.PySdkUtils; import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionResult; import com.intellij.execution.Executor; import com.intellij.execution.RunCanceledByUserException; import com.intellij.execution.configuration.EnvironmentVariablesData; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.filters.Filter; import com.intellij.execution.filters.TextConsoleBuilder; import com.intellij.execution.filters.UrlFilter; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.runners.ExecutionUtil; import com.intellij.execution.runners.ProgramRunner; import com.intellij.execution.ui.ConsoleView; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.util.PathUtil; import com.intellij.util.execution.ParametersListUtil; import com.jetbrains.python.console.PyDebugConsoleBuilder; import com.jetbrains.python.console.PythonDebugLanguageConsoleView; import com.jetbrains.python.run.CommandLinePatcher; import com.jetbrains.python.run.PythonConfigurationType; import com.jetbrains.python.run.PythonRunConfiguration; import com.jetbrains.python.run.PythonScriptCommandLineState; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import javax.annotation.Nullable; /** Python-specific run configuration runner. */ public class BlazePyRunConfigurationRunner implements BlazeCommandRunConfigurationRunner { /** This inserts flags provided by any BlazePyDebugHelpers to the pydevd.py invocation */ /** Used to store a runner to an {@link ExecutionEnvironment}. */ private static final Key<AtomicReference<File>> EXECUTABLE_KEY = Key.create("blaze.debug.py.executable"); /** Converts to the native python plugin debug configuration state */ static class BlazePyDummyRunProfileState implements RunProfileState { final BlazeCommandRunConfiguration configuration; BlazePyDummyRunProfileState(BlazeCommandRunConfiguration configuration) { this.configuration = configuration; } PythonScriptCommandLineState toNativeState(ExecutionEnvironment env) throws ExecutionException { File executable = env.getCopyableUserData(EXECUTABLE_KEY).get(); if (executable == null || StringUtil.isEmptyOrSpaces(executable.getPath())) { throw new ExecutionException("No blaze output script found"); } PythonRunConfiguration nativeConfig = (PythonRunConfiguration) PythonConfigurationType.getInstance() .getFactory() .createTemplateConfiguration(env.getProject()); nativeConfig.setScriptName(executable.getPath()); nativeConfig.setAddContentRoots(false); nativeConfig.setAddSourceRoots(false); nativeConfig.setWorkingDirectory( Strings.nullToEmpty( getRunfilesPath(executable, WorkspaceRoot.fromProjectSafe(env.getProject())))); Sdk sdk = PySdkUtils.getPythonSdk(env.getProject()); if (sdk == null) { throw new ExecutionException("Can't find a Python SDK when debugging a python target."); } nativeConfig.setModule(null); nativeConfig.setSdkHome(sdk.getHomePath()); BlazePyRunConfigState handlerState = configuration.getHandlerStateIfType(BlazePyRunConfigState.class); if (handlerState != null) { nativeConfig.setScriptParameters(Strings.emptyToNull(getScriptParams(handlerState))); EnvironmentVariablesData envState = handlerState.getEnvVarsState().getData(); nativeConfig.setPassParentEnvs(envState.isPassParentEnvs()); nativeConfig.setEnvs(envState.getEnvs()); } Label target = getSingleTarget(configuration); return new PythonScriptCommandLineState(nativeConfig, env) { private final CommandLinePatcher applyHelperPydevFlags = (commandLine) -> BlazePyDebugHelper.doBlazeDebugCommandlinePatching( nativeConfig.getProject(), target, commandLine); @Override protected ProcessHandler startProcess( PythonProcessStarter starter, @Nullable CommandLinePatcher... patchers) throws ExecutionException { // Need to run after the other CommandLinePatchers List<CommandLinePatcher> modifiedPatchers = new ArrayList<>(); if (patchers != null) { Collections.addAll(modifiedPatchers, patchers); } modifiedPatchers.add(applyHelperPydevFlags); return super.startProcess(starter, modifiedPatchers.toArray(new CommandLinePatcher[0])); } @Override public boolean isDebug() { return true; } @Override protected ConsoleView createAndAttachConsole( Project project, ProcessHandler processHandler, Executor executor) throws ExecutionException { ConsoleView consoleView = createConsoleBuilder(project, getSdk()).getConsole(); consoleView.addMessageFilter(createUrlFilter(processHandler)); consoleView.attachToProcess(processHandler); return consoleView; } @Override protected ProcessHandler doCreateProcess(GeneralCommandLine commandLine) throws ExecutionException { return super.doCreateProcess(ProcessGroupUtil.newProcessGroupFor(commandLine)); } }; } @Nullable @Override public ExecutionResult execute(Executor executor, ProgramRunner runner) throws ExecutionException { return null; } private static TextConsoleBuilder createConsoleBuilder(Project project, Sdk sdk) { return new PyDebugConsoleBuilder(project, sdk) { @Override protected ConsoleView createConsole() { PythonDebugLanguageConsoleView consoleView = new PythonDebugLanguageConsoleView(project, sdk); for (Filter filter : getFilters()) { consoleView.addMessageFilter(filter); } return consoleView; } }; } private static String getScriptParams(BlazeCommandRunConfigurationCommonState state) { List<String> params = Lists.newArrayList(state.getExeFlagsState().getFlagsForExternalProcesses()); params.addAll(state.getTestArgs()); String filterFlag = state.getTestFilterFlag(); if (filterFlag != null) { String testFilterArg = filterFlag.substring((BlazeFlags.TEST_FILTER + "=").length()); // testFilterArg is a space-delimited list of filters params.addAll(Splitter.on(" ").splitToList(testFilterArg)); } return ParametersListUtil.join(params); } } private static ImmutableList<Filter> getFilters() { return ImmutableList.<Filter>builder() .add(new BlazeTargetFilter(true)) .add(new UrlFilter()) .build(); } @Override public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment env) { if (!BlazeCommandRunConfigurationRunner.isDebugging(env) || BlazeCommandName.BUILD.equals(BlazeCommandRunConfigurationRunner.getBlazeCommand(env))) { return new BlazeCommandRunProfileState(env); } BlazeCommandRunConfiguration configuration = BlazeCommandRunConfigurationRunner.getConfiguration(env); env.putCopyableUserData(EXECUTABLE_KEY, new AtomicReference<>()); return new BlazePyDummyRunProfileState(configuration); } @Override public boolean executeBeforeRunTask(ExecutionEnvironment env) { if (!BlazeCommandRunConfigurationRunner.isDebugging(env) || BlazeCommandName.BUILD.equals(BlazeCommandRunConfigurationRunner.getBlazeCommand(env))) { return true; } env.getCopyableUserData(EXECUTABLE_KEY).set(null); try { File executable = getExecutableToDebug(env); env.getCopyableUserData(EXECUTABLE_KEY).set(executable); if (executable != null) { return true; } } catch (ExecutionException e) { ExecutionUtil.handleExecutionError( env.getProject(), env.getExecutor().getToolWindowId(), env.getRunProfile(), e); } return false; } /** Make a best-effort attempt to get the runfiles path. Returns null if it can't be found. */ @Nullable private static String getRunfilesPath(File executable, @Nullable WorkspaceRoot root) { if (root == null) { return null; } String workspaceName = root.directory().getName(); File expectedPath = new File(executable.getPath() + ".runfiles", workspaceName); if (FileOperationProvider.getInstance().exists(expectedPath)) { return expectedPath.getPath(); } return null; } private static Label getSingleTarget(BlazeCommandRunConfiguration config) throws ExecutionException { ImmutableList<? extends TargetExpression> targets = config.getTargets(); if (targets.size() != 1 || !(targets.get(0) instanceof Label)) { throw new ExecutionException("Invalid configuration: doesn't have a single target label"); } return (Label) targets.get(0); } /** * Builds blaze python target and returns the output build artifact. * * @throws ExecutionException if the target cannot be debugged. */ private static File getExecutableToDebug(ExecutionEnvironment env) throws ExecutionException { BlazeCommandRunConfiguration configuration = BlazeCommandRunConfigurationRunner.getConfiguration(env); Project project = configuration.getProject(); BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData(); if (blazeProjectData == null) { throw new ExecutionException("Not synced yet, please sync project"); } Label target = getSingleTarget(configuration); String validationError = BlazePyDebugHelper.validateDebugTarget(env.getProject(), target); if (validationError != null) { throw new WithBrowserHyperlinkExecutionException(validationError); } SaveUtil.saveAllFiles(); try (BuildResultHelper buildResultHelper = BuildResultHelperProvider.create(project)) { ListenableFuture<BuildResult> buildOperation = BlazeBeforeRunCommandHelper.runBlazeCommand( BlazeCommandName.BUILD, configuration, buildResultHelper, BlazePyDebugHelper.getAllBlazeDebugFlags(configuration.getProject(), target), ImmutableList.of(), BlazeInvocationContext.runConfigContext( ExecutorType.fromExecutor(env.getExecutor()), configuration.getType(), true), "Building debug binary"); try { BuildResult result = buildOperation.get(); if (result.status != BuildResult.Status.SUCCESS) { throw new ExecutionException("Blaze failure building debug binary"); } } catch (InterruptedException | CancellationException e) { buildOperation.cancel(true); throw new RunCanceledByUserException(); } catch (java.util.concurrent.ExecutionException e) { throw new ExecutionException(e); } List<File> candidateFiles; try { candidateFiles = BlazeArtifact.getLocalFiles( buildResultHelper.getBuildArtifactsForTarget(target, file -> true)) .stream() .filter(File::canExecute) .collect(Collectors.toList()); } catch (GetArtifactsException e) { throw new ExecutionException( String.format( "Failed to get output artifacts when building %s: %s", target, e.getMessage())); } if (candidateFiles.isEmpty()) { throw new ExecutionException( String.format("No output artifacts found when building %s", target)); } File file = findExecutable(target, candidateFiles); if (file == null) { throw new ExecutionException( String.format( "More than 1 executable was produced when building %s; " + "don't know which one to debug", target)); } LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(file)); return file; } } /** * Basic heuristic for choosing between multiple output files. Currently just looks for a filename * matching the target name. */ @VisibleForTesting @Nullable static File findExecutable(Label target, List<File> outputs) { if (outputs.size() == 1) { return outputs.get(0); } String name = PathUtil.getFileName(target.targetName().toString()); for (File file : outputs) { if (file.getName().equals(name)) { return file; } } return null; } }