From 1fed8ec138aef4bbb03701f47bdd16112ab622c5 Mon Sep 17 00:00:00 2001 From: Gunnar Wagenknecht Date: Sun, 13 Aug 2023 16:59:10 +0200 Subject: [PATCH] Support `bazel_binary` in project view --- .../DetectBazelVersionAndSetBinaryJob.java | 67 ++++++++++++++ .../EclipseHeadlessBazelCommandExecutor.java | 48 ++-------- .../model/BazelElementCommandExecutor.java | 62 ++++++++++--- .../eclipse/core/model/BazelModelManager.java | 2 +- .../eclipse/core/model/BazelPackage.java | 2 +- .../eclipse/core/model/BazelPackageInfo.java | 90 +++++++------------ .../eclipse/core/model/BazelWorkspace.java | 11 ++- .../core/model/BazelWorkspaceInfo.java | 52 ++++++++++- .../BazelModelCommandExecutionService.java | 13 ++- .../projectview/BazelProjectFileReader.java | 85 +++++++++++++----- .../core/projectview/BazelProjectView.java | 5 +- .../command/BazelBinaryVersionDetector.java | 24 ++++- .../sdk/command/BazelCommandExecutor.java | 2 +- 13 files changed, 320 insertions(+), 143 deletions(-) create mode 100644 bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/DetectBazelVersionAndSetBinaryJob.java diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/DetectBazelVersionAndSetBinaryJob.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/DetectBazelVersionAndSetBinaryJob.java new file mode 100644 index 00000000..a2a289e4 --- /dev/null +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/DetectBazelVersionAndSetBinaryJob.java @@ -0,0 +1,67 @@ +package com.salesforce.bazel.eclipse.core.extensions; + +import static java.lang.String.format; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; + +import com.salesforce.bazel.sdk.command.BazelBinary; +import com.salesforce.bazel.sdk.command.BazelBinaryVersionDetector; + +/** + * Calls bazel --version on a provided binary to identify the version use. + *

+ * Calls {@link EclipseHeadlessBazelCommandExecutor#setBazelBinary(BazelBinary)} when done. + *

+ */ +public final class DetectBazelVersionAndSetBinaryJob extends Job { + private final Path binary; + private final boolean wrapExecutionIntoShell; + private final Consumer binaryConsumer; + private final Supplier fallbackSupplier; + + /** + * @param binary + * the binary to test + * @param wrapExecutionIntoShell + * true if shell wrapping is desired, false otherwise + * @param binaryConsumer + * receiver of the binary including the detected version (will be called with a fallback value in + * case of errors) + * @param fallbackSupplier + * supplier for fallback value in case of errors + */ + public DetectBazelVersionAndSetBinaryJob(Path binary, boolean wrapExecutionIntoShell, + Consumer binaryConsumer, Supplier fallbackSupplier) { + super(format("Detecting Bazel version...", binary)); + this.binary = binary; + this.wrapExecutionIntoShell = wrapExecutionIntoShell; + this.binaryConsumer = binaryConsumer; + this.fallbackSupplier = fallbackSupplier; + setSystem(true); + setPriority(SHORT); + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + var bazelVersion = new BazelBinaryVersionDetector(binary, wrapExecutionIntoShell).detectVersion(); + binaryConsumer.accept(new BazelBinary(binary, bazelVersion)); + return Status.OK_STATUS; + } catch (IOException e) { + binaryConsumer.accept(fallbackSupplier.get()); + return Status.error(format("Unable to detect Bazel version of binary '%s'!", binary), e); + } catch (InterruptedException e) { + EclipseHeadlessBazelCommandExecutor.LOG.warn("Interrupted waiting for bazel --version to respond for binary '{}'", binary, e); + binaryConsumer.accept(fallbackSupplier.get()); + return Status.CANCEL_STATUS; + } + } +} \ No newline at end of file diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/EclipseHeadlessBazelCommandExecutor.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/EclipseHeadlessBazelCommandExecutor.java index 8b61cf75..8473fef5 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/EclipseHeadlessBazelCommandExecutor.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/extensions/EclipseHeadlessBazelCommandExecutor.java @@ -5,15 +5,11 @@ import static com.salesforce.bazel.eclipse.preferences.BazelCorePreferenceKeys.PREF_KEY_USE_SHELL_ENVIRONMENT; import static java.lang.String.format; -import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; @@ -26,7 +22,6 @@ import com.salesforce.bazel.eclipse.preferences.BazelCorePreferenceKeys; import com.salesforce.bazel.sdk.BazelVersion; import com.salesforce.bazel.sdk.command.BazelBinary; -import com.salesforce.bazel.sdk.command.BazelBinaryVersionDetector; import com.salesforce.bazel.sdk.command.DefaultBazelCommandExecutor; /** @@ -34,39 +29,6 @@ */ public class EclipseHeadlessBazelCommandExecutor extends DefaultBazelCommandExecutor { - /** - * Calls bazel --version on a provided binary to identify the version use. - *

- * Calls {@link EclipseHeadlessBazelCommandExecutor#setBazelBinary(BazelBinary)} when done. - *

- */ - protected final class DetectBazelVersionAndSetBinaryJob extends Job { - private final Path binary; - - private DetectBazelVersionAndSetBinaryJob(Path binary) { - super(format("Detecting Bazel version...", binary)); - this.binary = binary; - setSystem(true); - setPriority(SHORT); - } - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - var bazelVersion = new BazelBinaryVersionDetector(binary, isWrapExecutionIntoShell()).detectVersion(); - setBazelBinary(new BazelBinary(binary, bazelVersion)); - return Status.OK_STATUS; - } catch (IOException e) { - setBazelBinary(UNKNOWN_BAZEL_BINARY); - return Status.error(format("Unable to detect Bazel version of binary '%s'!", binary), e); - } catch (InterruptedException e) { - LOG.warn("Interrupted waiting for bazel --version to respond for binary '{}'", binary, e); - setBazelBinary(UNKNOWN_BAZEL_BINARY); - return Status.CANCEL_STATUS; - } - } - } - static BazelBinary UNKNOWN_BAZEL_BINARY = new BazelBinary(Path.of("bazel"), new BazelVersion(999, 999, 999)); static Logger LOG = LoggerFactory.getLogger(EclipseHeadlessBazelCommandExecutor.class); @@ -168,14 +130,20 @@ protected void initializeWrapExecutionIntoShellSettingFromPreferences() { */ protected Job scheduleInitBazelBinaryFromPreferencesJob() { var binary = Path.of(Platform.getPreferencesService().get(PREF_KEY_BAZEL_BINARY, "bazel", preferencesLookup)); - var job = new DetectBazelVersionAndSetBinaryJob(binary); + var job = new DetectBazelVersionAndSetBinaryJob( + binary, + isWrapExecutionIntoShell(), + this::setBazelBinary, + () -> UNKNOWN_BAZEL_BINARY); job.schedule(); return job; } @Override public void setBazelBinary(BazelBinary bazelBinary) { - LOG.info("Using default Bazel binary '{}' (version '{}')", bazelBinary.executable(), + LOG.info( + "Using default Bazel binary '{}' (version '{}')", + bazelBinary.executable(), bazelBinary.bazelVersion()); super.setBazelBinary(bazelBinary); } diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelElementCommandExecutor.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelElementCommandExecutor.java index 7db6a79f..68f0dffc 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelElementCommandExecutor.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelElementCommandExecutor.java @@ -9,12 +9,14 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.core.runtime.jobs.Job; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,7 +49,8 @@ public BazelElementCommandExecutor(BazelElement executionContext) { * Applies any Bazel workspace specific configuration to the command binary. *

* By default we have an Eclipse wide preference setting providing the Eclipse wide Bazel binary to use. However, a - * Bazel workspace may use a different Bazel version (eg., via .bazelversion file). + * Bazel workspace may use a different Bazel version (eg., via .bazelversion file or + * .bazelproject project view). *

*

* We rely on a single, system wide Bazel binary (eg., Bazelisk or Bazel shell wrapper script) to resolve the @@ -60,14 +63,22 @@ public BazelElementCommandExecutor(BazelElement executionContext) { * the workspace to use * @throws CoreException */ - private void configureWithWorkspaceBazelVersion(BazelCommand command, BazelWorkspace bazelWorkspace) - throws CoreException { - var bazelBinary = getExecutionService().getBazelBinary(); + private void configureCommand(BazelCommand command, BazelWorkspace bazelWorkspace) throws CoreException { + var bazelBinary = bazelWorkspace.getBazelBinary(); + if (bazelBinary == null) { + bazelBinary = getExecutionService().getBazelBinary(); + } else { + LOG.trace("Using binary from workspace: {}", bazelBinary); + } var workspaceBazelVersion = bazelWorkspace.getBazelVersion(); if (!bazelBinary.bazelVersion().equals(workspaceBazelVersion)) { - LOG.trace("Using workspace Bazel version '{}' for command: {}", workspaceBazelVersion, command); - command.setBazelBinary( - new BazelBinary(getExecutionService().getBazelBinary().executable(), workspaceBazelVersion)); + LOG.trace( + "Forcing (overriding) workspace Bazel version '{}' for command: {}", + workspaceBazelVersion, + command); + command.setBazelBinary(new BazelBinary(bazelBinary.executable(), workspaceBazelVersion)); + } else { + command.setBazelBinary(bazelBinary); } } @@ -107,28 +118,32 @@ final BazelModelCommandExecutionService getExecutionService() { */ public R runDirectlyWithWorkspaceLock(BazelCommand command, List resourcesToRefresh, IProgressMonitor monitor) throws CoreException { - configureWithWorkspaceBazelVersion(command, executionContext.getBazelWorkspace()); + configureCommand(command, executionContext.getBazelWorkspace()); return getExecutionService().executeWithWorkspaceLock(command, executionContext, resourcesToRefresh, monitor); } /** - * Execute a Bazel query using + * Execute a Bazel query command using * {@link BazelModelCommandExecutionService#executeOutsideWorkspaceLockAsync(BazelCommand, BazelElement)}. *

- * The method will block and wait for the result. + * The method will block the current thread and wait for the result. However, the command execution will happen in a + * different {@link Job thread} in the background for proper progress handling/reporting. + *

+ *

+ * Note, the command must not modify any resources in the workspace (eg., performing a build or something). *

* * @param * the command result type * @param command - * the query command to execute + * the command to execute * @return the command result (never null) * @see BazelModelCommandExecutionService#executeOutsideWorkspaceLockAsync(BazelCommand, BazelElement) * executeOutsideWorkspaceLockAsync for execution and locking semantics * @throws CoreException */ public R runQueryWithoutLock(BazelQueryCommand command) throws CoreException { - configureWithWorkspaceBazelVersion(command, executionContext.getBazelWorkspace()); + configureCommand(command, executionContext.getBazelWorkspace()); Future future = getExecutionService().executeOutsideWorkspaceLockAsync(command, executionContext); try { return future.get(); @@ -144,9 +159,30 @@ public R runQueryWithoutLock(BazelQueryCommand command) throws CoreExcept } } + /** + * Execute a Bazel command using + * {@link BazelModelCommandExecutionService#executeOutsideWorkspaceLockAsync(BazelCommand, BazelElement)}. + *

+ * The method will block the current thread and wait for the result. However, the command execution will happen in a + * different {@link Job thread} in the background for proper progress handling/reporting. + *

+ * + * @param + * the command result type + * @param command + * the command to execute + * @param rule + * the scheduling rule to apply to {@link WorkspaceJob#setRule(ISchedulingRule)} + * @param resourcesToRefresh + * list of resources to refresh recursively when the command execution is complete + * @return the command result (never null) + * @see BazelModelCommandExecutionService#executeWithWorkspaceLockAsync(BazelCommand, BazelElement, ISchedulingRule, + * List) executeWithWorkspaceLockAsync for execution and locking semantics + * @throws CoreException + */ public R runWithWorkspaceLock(BazelCommand command, ISchedulingRule rule, List resourcesToRefresh) throws CoreException { - configureWithWorkspaceBazelVersion(command, executionContext.getBazelWorkspace()); + configureCommand(command, executionContext.getBazelWorkspace()); Future future = getExecutionService() .executeWithWorkspaceLockAsync(command, executionContext, rule, resourcesToRefresh); try { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelModelManager.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelModelManager.java index 745e18ce..9834b127 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelModelManager.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelModelManager.java @@ -136,7 +136,7 @@ public BazelClasspathManager getClasspathManager() { /** * @return the execution service used by the model */ - public BazelModelCommandExecutionService getExecutionService() { + BazelModelCommandExecutionService getExecutionService() { return executionService; } diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackage.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackage.java index dd2526b6..4e458516 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackage.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackage.java @@ -89,7 +89,7 @@ protected BazelPackageInfo createInfo() throws CoreException { Status.error(format("Package '%s' does not exist in workspace '%s'!", label, parent.getName()))); } - var targets = BazelPackageInfo.queryForTargets(this, getModelManager().getExecutionService()); + var targets = BazelPackageInfo.queryForTargets(this, getCommandExecutor()); return new BazelPackageInfo(buildFile, this, targets); } diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackageInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackageInfo.java index b780fee4..7a3ac8c6 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackageInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelPackageInfo.java @@ -26,17 +26,14 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutionException; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.devtools.build.lib.query2.proto.proto2api.Build.Target; -import com.salesforce.bazel.eclipse.core.model.execution.BazelModelCommandExecutionService; import com.salesforce.bazel.sdk.command.BazelQueryForTargetProtoCommand; import com.salesforce.bazel.sdk.model.BazelLabel; @@ -73,15 +70,16 @@ && hasOwnerPropertySetForLabel(project, bazelPackage.getLabel()) // represents t } static Map queryForTargets(BazelPackage bazelPackage, - BazelModelCommandExecutionService executionService) throws CoreException { + BazelElementCommandExecutor bazelElementCommandExecutor) throws CoreException { - var result = queryForTargets(bazelPackage.getBazelWorkspace(), List.of(bazelPackage), executionService) - .get(bazelPackage); + var result = + queryForTargets(bazelPackage.getBazelWorkspace(), List.of(bazelPackage), bazelElementCommandExecutor) + .get(bazelPackage); return result != null ? result : Collections.emptyMap(); } static Map> queryForTargets(BazelWorkspace bazelWorkspace, - Collection bazelPackages, BazelModelCommandExecutionService executionService) + Collection bazelPackages, BazelElementCommandExecutor bazelElementCommandExecutor) throws CoreException { // bazel query '"//foo:all" + "//bar:all"' @@ -99,58 +97,38 @@ static Map> queryForTargets(BazelWorkspace baz .forEach(p -> bazelPackageByWorkspaceRelativePath.put(p.getWorkspaceRelativePath().toString(), p)); Map> result = new HashMap<>(); - try { - LOG.debug("{}: querying Bazel for list of targets from: {}", bazelWorkspace, query); - var queryResult = - executionService - .executeOutsideWorkspaceLockAsync( - new BazelQueryForTargetProtoCommand( - workspaceRoot, - query, - true /* keep going */, - format( - "Loading targets for %d %s", - bazelPackages.size(), - bazelPackages.size() == 1 ? "package" : "packages")), - bazelWorkspace) - .get(); - for (Target target : queryResult) { - if (!target.hasRule()) { - LOG.trace("{}: ignoring target: {}", bazelWorkspace, target); - continue; - } - - LOG.trace("{}: found target: {}", bazelWorkspace, target); - var targetLabel = new BazelLabel(target.getRule().getName()); - - var bazelPackage = bazelPackageByWorkspaceRelativePath.get(targetLabel.getPackagePath()); - if (bazelPackage == null) { - LOG.debug("{}: ignoring target for unknown package: {}", bazelWorkspace, targetLabel); - continue; - } - if (!result.containsKey(bazelPackage)) { - result.put(bazelPackage, new HashMap<>()); - } - - var targetName = targetLabel.getTargetName(); - result.get(bazelPackage).put(targetName, target); + LOG.debug("{}: querying Bazel for list of targets from: {}", bazelWorkspace, query); + var queryResult = bazelElementCommandExecutor.runQueryWithoutLock( + new BazelQueryForTargetProtoCommand( + workspaceRoot, + query, + true /* keep going */, + format( + "Loading targets for %d %s", + bazelPackages.size(), + bazelPackages.size() == 1 ? "package" : "packages"))); + for (Target target : queryResult) { + if (!target.hasRule()) { + LOG.trace("{}: ignoring target: {}", bazelWorkspace, target); + continue; } - return result; - } catch (InterruptedException e) { - throw new OperationCanceledException("cancelled"); - } catch (ExecutionException e) { - var cause = e.getCause(); - if (cause == null) { - throw new CoreException( - Status.error( - format("bazel query failed in workspace '%s' for with unknown reason", workspaceRoot), - e)); + + LOG.trace("{}: found target: {}", bazelWorkspace, target); + var targetLabel = new BazelLabel(target.getRule().getName()); + + var bazelPackage = bazelPackageByWorkspaceRelativePath.get(targetLabel.getPackagePath()); + if (bazelPackage == null) { + LOG.debug("{}: ignoring target for unknown package: {}", bazelWorkspace, targetLabel); + continue; } - throw new CoreException( - Status.error( - format("bazel query failed in workspace '%s': %s", workspaceRoot, cause.getMessage()), - cause)); + if (!result.containsKey(bazelPackage)) { + result.put(bazelPackage, new HashMap<>()); + } + + var targetName = targetLabel.getTargetName(); + result.get(bazelPackage).put(targetName, target); } + return result; } private final Path buildFile; diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java index 386f16e8..1bd1cd78 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java @@ -39,6 +39,7 @@ import com.salesforce.bazel.eclipse.core.projectview.BazelProjectView; import com.salesforce.bazel.sdk.BazelVersion; +import com.salesforce.bazel.sdk.command.BazelBinary; import com.salesforce.bazel.sdk.model.BazelLabel; /** @@ -190,6 +191,14 @@ public boolean exists() { return isDirectory(path) && (findWorkspaceFile(path) != null); } + /** + * @return the {@link BazelBinary} specified by the project view (or null if a default should be used) + * @throws CoreException + */ + BazelBinary getBazelBinary() throws CoreException { + return getInfo().getBazelBinary(); + } + /** * Returns a Bazel package for the given label. *

@@ -432,7 +441,7 @@ public void open(Collection bazelPackages) throws CoreException { } // open all closed projects - var targetsByPackage = queryForTargets(this, closedPackages, getModelManager().getExecutionService()); + var targetsByPackage = queryForTargets(this, closedPackages, getCommandExecutor()); for (BazelPackage bazelPackage : closedPackages) { var targets = targetsByPackage.get(bazelPackage); if (targets == null) { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspaceInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspaceInfo.java index 2a471964..10e2cf7f 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspaceInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspaceInfo.java @@ -31,17 +31,23 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.devtools.build.lib.query2.proto.proto2api.Build.Target; +import com.salesforce.bazel.eclipse.core.extensions.DetectBazelVersionAndSetBinaryJob; import com.salesforce.bazel.eclipse.core.model.execution.BazelModelCommandExecutionService; import com.salesforce.bazel.eclipse.core.projectview.BazelProjectFileReader; import com.salesforce.bazel.eclipse.core.projectview.BazelProjectView; import com.salesforce.bazel.sdk.BazelVersion; +import com.salesforce.bazel.sdk.command.BazelBinary; import com.salesforce.bazel.sdk.command.BazelInfoCommand; import com.salesforce.bazel.sdk.command.BazelQueryForTargetProtoCommand; public final class BazelWorkspaceInfo extends BazelElementInfo { + private static Logger LOG = LoggerFactory.getLogger(BazelWorkspaceInfo.class); + private static final String RELEASE_VERSION_PREFIX = "release "; private final IPath root; @@ -66,6 +72,8 @@ public final class BazelWorkspaceInfo extends BazelElementInfo { private volatile Map externalRepositoryRuleByName; + private BazelBinary bazelBinary; + public BazelWorkspaceInfo(IPath root, Path workspaceFile, BazelWorkspace bazelWorkspace) { this.root = root; this.workspaceFile = workspaceFile; @@ -76,6 +84,10 @@ public IPath getBazelBin() { return bazelBin; } + BazelBinary getBazelBinary() { + return bazelBinary; + } + public IPath getBazelGenfiles() { return bazelGenfiles; } @@ -232,10 +244,47 @@ public String getWorkspaceName() { return requireNonNull(name, "not loaded"); } + private void initializeBazelBinary(Path binary) { + // we should not be using any service but just execute it directly + // because BazelElementCommandExecutor calls BazelWorksapce#getBazelBinary + + // use job to signal progress + var job = new DetectBazelVersionAndSetBinaryJob(binary, false, bazelBinary -> { + this.bazelBinary = bazelBinary; + }, () -> { + var defaultVersion = new BazelVersion(999, 999, 999); + LOG.error( + "Unable to detect version for Bazel binary '{}' (configured via .bazelproject file) - defaulting to '{}'", + binary, + defaultVersion); + return new BazelBinary(binary, defaultVersion); + }); + job.schedule(); + try { + // wait for completion + job.join(); + } catch (InterruptedException e) { + throw new OperationCanceledException("Interrupted waiting for Bazel binary version detection to happen"); + } + } + public void load(BazelModelCommandExecutionService executionService) throws CoreException { var workspaceRoot = getWorkspaceFile().getParent(); + + // check for a workspace specific binary + // note: this will trigger loading the project view + var workspaceBinary = getBazelProjectView().bazelBinary(); + if (workspaceBinary != null) { + // resolve against the workspace root + var binary = workspaceBinary.isAbsolute() ? workspaceBinary.toPath() + : workspaceRoot.resolve(workspaceBinary.toPath()); + initializeBazelBinary(binary); + } + try { - // we use the BazelModelCommandExecutionService directly because info is not a query + // we use the BazelModelCommandExecutionService directly because there is a cycle dependency between + // BazelModelCommandExecutor and BazelWorkspace#getBazelBinary + var infoResult = executionService .executeOutsideWorkspaceLockAsync( @@ -265,6 +314,7 @@ public void load(BazelModelCommandExecutionService executionService) throws Core outputPath = getExpectedOutputAsPath(infoResult, "output_path"); if (release.startsWith(RELEASE_VERSION_PREFIX)) { + // parse the version from bazel info instead of using BazelBinary (if available) bazelVersion = BazelVersion.parseVersion(release.substring(RELEASE_VERSION_PREFIX.length())); } } catch (InterruptedException e) { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/execution/BazelModelCommandExecutionService.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/execution/BazelModelCommandExecutionService.java index 5dc14911..97125669 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/execution/BazelModelCommandExecutionService.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/execution/BazelModelCommandExecutionService.java @@ -26,6 +26,7 @@ import org.eclipse.core.runtime.jobs.Job; import com.salesforce.bazel.eclipse.core.model.BazelElement; +import com.salesforce.bazel.eclipse.core.model.BazelElementCommandExecutor; import com.salesforce.bazel.eclipse.core.model.BazelModel; import com.salesforce.bazel.eclipse.core.model.BazelWorkspace; import com.salesforce.bazel.sdk.command.BazelBinary; @@ -40,11 +41,17 @@ * jobs will ensure they never compete with each other and especially never conflict with ongoing build activity inside * Eclipse. *

+ *

+ * Note, callers should use this service very seldom. Instead the {@link BazelElementCommandExecutor} should be used via + * {@link BazelElement#getCommandExecutor()}. + *

+ * + * @see BazelElementCommandExecutor */ public interface BazelModelCommandExecutionService { /** - * Execute a read-only Bazel command. + * Execute a read-only Bazel command asynchronously. *

* The command is expected to be read-only from an Eclipse resource perspective, no resources will be modified when * executing this command. This distinction is important from an Eclipse perspective to still allow scheduling @@ -102,7 +109,7 @@ R executeWithWorkspaceLock(BazelCommand command, BazelElement execu List resourcesToRefresh, IProgressMonitor monitor) throws CoreException; /** - * Execute a potentially resource modifying Bazel command. + * Execute a potentially resource modifying Bazel command asynchronously. *

* The command is allowed to modify Eclipse resources. As such a resource level lock will be obtained by the * execution service before executing the command. The lock ensures that the workspace is locked for modification @@ -144,5 +151,5 @@ Future executeWithWorkspaceLockAsync(BazelCommand command, BazelElemen * @throws NullPointerException * if the command executor has no Bazel binary */ - BazelBinary getBazelBinary() throws NullPointerException; + BazelBinary getBazelBinary(); } diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectFileReader.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectFileReader.java index eba714de..e612b238 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectFileReader.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectFileReader.java @@ -14,6 +14,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.eclipse.core.runtime.IPath; + import com.google.idea.blaze.base.model.primitives.InvalidTargetException; import com.google.idea.blaze.base.model.primitives.TargetExpression; import com.google.idea.blaze.base.model.primitives.WorkspacePath; @@ -58,6 +60,7 @@ public void close() { LinkedHashSet tsConfigRules = new LinkedHashSet<>(); String targetDiscoveryStrategy, targetProvisioningStrategy; final LinkedHashSet importingFiles = new LinkedHashSet<>(); + IPath bazelBinary; public BazelProjectView build() throws IllegalStateException { // check mandatory parameters @@ -95,16 +98,29 @@ public BazelProjectView build() throws IllegalStateException { "at least one target to include is required unless derive_targets_from_directories is set"); } - return new BazelProjectView(directoriesToImport, directoriesToExclude, targetsList, - deriveTargetsFromDirectories, workspaceType, additionalLanguages, javaLanguageLevel, tsConfigRules, - targetDiscoveryStrategy, targetProvisioningStrategy); + return new BazelProjectView( + directoriesToImport, + directoriesToExclude, + targetsList, + deriveTargetsFromDirectories, + workspaceType, + additionalLanguages, + javaLanguageLevel, + tsConfigRules, + targetDiscoveryStrategy, + targetProvisioningStrategy, + bazelBinary); } public ImportHandle startImporting(Path bazelProjectViewFile) throws IOException { if (!importingFiles.add(bazelProjectViewFile)) { - throw new IOException(format("Recursive import detected for file '%s'%n%s", bazelProjectViewFile, - importingFiles.stream().map(Path::toString) - .collect(joining(System.lineSeparator() + "-> ", "-> ", "")))); + throw new IOException( + format( + "Recursive import detected for file '%s'%n%s", + bazelProjectViewFile, + importingFiles.stream() + .map(Path::toString) + .collect(joining(System.lineSeparator() + "-> ", "-> ", "")))); } return new ImportHandle(bazelProjectViewFile); } @@ -179,21 +195,28 @@ private void parseProjectFile(Path bazelProjectFile, BazelProjectViewBuilder bui fileToImport = rawSection.getBodyAsPath(); } catch (NullPointerException e) { throw new IOException( - format("Invalid syntax in '%s': import needs a value!", bazelProjectFile), e); + format("Invalid syntax in '%s': import needs a value!", bazelProjectFile), + e); } if (fileToImport.isAbsolute()) { - throw new IOException(format( - "Invalid import (%s) defined in '%s': imports must be relative to the file they are defined in", - fileToImport, bazelProjectFile)); + throw new IOException( + format( + "Invalid import (%s) defined in '%s': imports must be relative to the file they are defined in", + fileToImport, + bazelProjectFile)); } var resolvedPathOfFileToImport = bazelWorkspaceRoot.resolve(fileToImport); try { // parse the import into the existing builder (this allows to implement the wanted behavior) parseProjectFile(resolvedPathOfFileToImport, builder); } catch (NoSuchFileException e) { - throw new NoSuchFileException(resolvedPathOfFileToImport.toString(), - bazelProjectFile.toString(), format("import '%s' not found (defined in '%s')", - resolvedPathOfFileToImport, bazelProjectFile)); + throw new NoSuchFileException( + resolvedPathOfFileToImport.toString(), + bazelProjectFile.toString(), + format( + "import '%s' not found (defined in '%s')", + resolvedPathOfFileToImport, + bazelProjectFile)); } break; } @@ -207,6 +230,16 @@ private void parseProjectFile(Path bazelProjectFile, BazelProjectViewBuilder bui } break; } + case "bazel_binary": { + try { + builder.bazelBinary = IPath.forPosix(rawSection.getBodyAsSingleValue()); + } catch (NullPointerException e) { + throw new IOException( + format("Invalid syntax in '%s': bazel_binary needs a value!", bazelProjectFile), + e); + } + break; + } case "additional_languages": { parseSectionBodyIntoList(rawSection).forEach(builder.additionalLanguages::add); break; @@ -215,8 +248,11 @@ private void parseProjectFile(Path bazelProjectFile, BazelProjectViewBuilder bui try { builder.javaLanguageLevel = rawSection.getBodyAsSingleValue(); } catch (NullPointerException e) { - throw new IOException(format("Invalid syntax in '%s': java_language_level needs a value!", - bazelProjectFile), e); + throw new IOException( + format( + "Invalid syntax in '%s': java_language_level needs a value!", + bazelProjectFile), + e); } break; } @@ -230,7 +266,8 @@ private void parseProjectFile(Path bazelProjectFile, BazelProjectViewBuilder bui builder.targetDiscoveryStrategy = rawSection.getBodyAsSingleValue(); } catch (NullPointerException e) { throw new IOException( - format("Invalid syntax in '%s': target_discovery_strategy needs a value!", + format( + "Invalid syntax in '%s': target_discovery_strategy needs a value!", bazelProjectFile), e); } @@ -242,7 +279,8 @@ private void parseProjectFile(Path bazelProjectFile, BazelProjectViewBuilder bui builder.targetProvisioningStrategy = rawSection.getBodyAsSingleValue(); } catch (NullPointerException e) { throw new IOException( - format("Invalid syntax in '%s': target_provisioning_strategy needs a value!", + format( + "Invalid syntax in '%s': target_provisioning_strategy needs a value!", bazelProjectFile), e); } @@ -262,13 +300,18 @@ private void parseProjectFile(Path bazelProjectFile, BazelProjectViewBuilder bui private List parseRawSections(String bazelProjectFileContent) throws IOException { List results = new ArrayList<>(); - var headers = SECTION_HEADER_REGEX.matcher(bazelProjectFileContent).results().map(t -> t.group(2)) + var headers = SECTION_HEADER_REGEX.matcher(bazelProjectFileContent) + .results() + .map(t -> t.group(2)) .toArray(String[]::new); var bodies = SECTION_HEADER_REGEX.split(bazelProjectFileContent); if (headers.length != (bodies.length - 1)) { - throw new IOException(format( - "Syntax error in .bazelproject: The number of section headers doesn't match the number of section bodies (%d != %d; header: %s).", - headers.length, bodies.length, Stream.of(headers).collect(joining(", ")))); + throw new IOException( + format( + "Syntax error in .bazelproject: The number of section headers doesn't match the number of section bodies (%d != %d; header: %s).", + headers.length, + bodies.length, + Stream.of(headers).collect(joining(", ")))); } for (var i = 0; i < headers.length; i++) { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectView.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectView.java index 5ae70060..0fac9b09 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectView.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/projectview/BazelProjectView.java @@ -3,6 +3,8 @@ import java.util.Collection; import java.util.Collections; +import org.eclipse.core.runtime.IPath; + import com.google.idea.blaze.base.model.primitives.TargetExpression; import com.google.idea.blaze.base.model.primitives.WorkspacePath; @@ -31,7 +33,8 @@ public record BazelProjectView( String javaLanguageLevel, Collection tsConfigRules, String targetDiscoveryStrategy, - String targetProvisioningStrategy) { + String targetProvisioningStrategy, + IPath bazelBinary) { public BazelProjectView { directoriesToImport = Collections.unmodifiableCollection(directoriesToImport); diff --git a/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelBinaryVersionDetector.java b/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelBinaryVersionDetector.java index 5d169c2e..e2a73cd6 100644 --- a/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelBinaryVersionDetector.java +++ b/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelBinaryVersionDetector.java @@ -11,6 +11,9 @@ import java.nio.file.Path; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.salesforce.bazel.sdk.BazelVersion; import com.salesforce.bazel.sdk.command.shell.ShellUtil; @@ -19,6 +22,8 @@ */ public class BazelBinaryVersionDetector { + private static Logger LOG = LoggerFactory.getLogger(BazelBinaryVersionDetector.class); + private static final String BAZEL_VERSION_PREFIX = "bazel "; private final Path binary; @@ -41,6 +46,9 @@ public BazelVersion detectVersion() throws IOException, InterruptedException { if (useShellEnvironment) { commandLine = shellUtil.wrapExecutionIntoShell(commandLine); } + if (LOG.isDebugEnabled()) { + LOG.debug("Checking Bazel binary '{}' for its version: {}", binary, commandLine); + } var pb = new ProcessBuilder(commandLine); var stdoutFile = File.createTempFile("bazel_version_", ".txt"); pb.redirectOutput(stdoutFile); @@ -49,8 +57,12 @@ public BazelVersion detectVersion() throws IOException, InterruptedException { var result = pb.start().waitFor(); if (result != 0) { var out = readString(stderrFile.toPath(), Charset.defaultCharset()); - throw new IOException(format("Error executing '%s'. Process exited with code %d: %s", - commandLine.stream().collect(joining(" ")), result, out)); + throw new IOException( + format( + "Error executing '%s'. Process exited with code %d: %s", + commandLine.stream().collect(joining(" ")), + result, + out)); } var lines = readAllLines(stdoutFile.toPath(), Charset.defaultCharset()); for (String potentialVersion : lines) { @@ -60,7 +72,11 @@ public BazelVersion detectVersion() throws IOException, InterruptedException { } var out = readString(stdoutFile.toPath(), Charset.defaultCharset()); - throw new IOException(format("No version information found in output of command '%s': %s", - commandLine.stream().collect(joining(" ")), result, out)); + throw new IOException( + format( + "No version information found in output of command '%s': %s", + commandLine.stream().collect(joining(" ")), + result, + out)); } } diff --git a/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelCommandExecutor.java b/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelCommandExecutor.java index d7057532..beb2a63c 100644 --- a/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelCommandExecutor.java +++ b/bundles/com.salesforce.bazel.sdk/src/com/salesforce/bazel/sdk/command/BazelCommandExecutor.java @@ -43,6 +43,6 @@ interface CancelationCallback { * @throws NullPointerException * if the command executor has no Bazel binary */ - BazelBinary getBazelBinary() throws NullPointerException; + BazelBinary getBazelBinary(); }