You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by en...@apache.org on 2021/09/30 10:56:41 UTC

[netbeans] branch master updated: Support of setting the current working directory and environment variables for Java8+ VSCode projects.

This is an automated email from the ASF dual-hosted git repository.

entl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new 03bd150  Support of setting the current working directory and environment variables for Java8+ VSCode projects.
03bd150 is described below

commit 03bd150b8b96fde89f653c9f581a24ba1af411e2
Author: Martin Entlicher <ma...@oracle.com>
AuthorDate: Tue Sep 21 03:03:14 2021 +0200

    Support of setting the current working directory and environment variables for Java8+ VSCode projects.
---
 cpplite/cpplite.debugger/nbproject/project.xml     |  11 +-
 .../modules/cpplite/debugger/CPPLiteDebugger.java  |  36 +++-
 .../cpplite/debugger/CPPLiteDebuggerConfig.java    |  35 +++-
 .../modules/cpplite/debugger/api/Debugger.java     |   4 +-
 .../debugger/ni/NIDebuggerProviderImpl.java        |  20 ++-
 .../cpplite/debugger/AbstractDebugTest.java        |  53 +++++-
 .../cpplite/debugger/ProcessParametersTest.java    | 192 +++++++++++++++++++++
 extide/gradle/apichanges.xml                       |  17 ++
 extide/gradle/arch.xml                             |  14 ++
 .../gradle/tooling/NetBeansRunSinglePlugin.groovy  | 104 ++++++++++-
 .../base/ExplicitProcessParameters.java            | 106 +++++++++++-
 .../nativeimage/api/debug/EvaluateException.java   |   7 +-
 .../modules/nativeimage/api/debug/NIDebugger.java  |  56 ++++--
 .../modules/nativeimage/api/debug/NIFrame.java     |  16 +-
 .../api/debug/NILineBreakpointDescriptor.java      |  26 +--
 .../modules/nativeimage/api/debug/NIVariable.java  |  20 +--
 .../api/debug/StartDebugParameters.java            | 188 ++++++++++++++++++++
 .../nativeimage/spi/debug/NIDebuggerProvider.java  |  56 ++++--
 java/gradle.java/arch.xml                          |  12 ++
 java/gradle.java/nbproject/project.xml             |   4 +-
 .../modules/gradle/java/action-mapping.xml         |  12 +-
 .../modules/gradle/java/api/ProjectActions.java    |  23 ++-
 .../gradle/java/execute/JavaExecTokenProvider.java |  58 ++++++-
 .../java/execute/JavaExecTokenProviderTest.java    |  34 ++++
 java/java.lsp.server/nbproject/project.xml         |   4 +-
 .../server/debugging/launch/NbLaunchDelegate.java  |  89 ++++++++--
 java/java.lsp.server/vscode/package.json           |  15 ++
 .../nbproject/project.xml                          |   2 +-
 .../debugger/actions/NIAttachCustomizer.java       |   1 +
 .../nativeimage/debugger/api/NIDebugRunner.java    |  25 +++
 java/maven/nbproject/project.xml                   |   2 +-
 .../modules/maven/debug/DebuggerChecker.java       |  18 +-
 .../maven/execute/DefaultReplaceTokenProvider.java |   1 +
 .../maven/execute/MavenCommandLineExecutor.java    |  14 +-
 .../maven/runjar/LaunchArgPrereqsChecker.java      |  15 ++
 .../modules/maven/runjar/MavenExecuteUtils.java    |   7 +
 .../modules/maven/runjar/RunJarStartupArgs.java    |  18 +-
 .../maven/execute/MavenExecutionTestBase.java      |   6 +-
 .../maven/runjar/ExecutionEnvHelperTest.java       |  52 ++++++
 39 files changed, 1242 insertions(+), 131 deletions(-)

diff --git a/cpplite/cpplite.debugger/nbproject/project.xml b/cpplite/cpplite.debugger/nbproject/project.xml
index 7574c06..55bb7e8 100644
--- a/cpplite/cpplite.debugger/nbproject/project.xml
+++ b/cpplite/cpplite.debugger/nbproject/project.xml
@@ -61,12 +61,21 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
+                    <code-name-base>org.netbeans.modules.extexecution.base</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>2</release-version>
+                        <specification-version>1.20</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     <code-name-base>org.netbeans.modules.nativeimage.api</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>0</release-version>
-                        <specification-version>0.4</specification-version>
+                        <specification-version>0.5</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebugger.java b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebugger.java
index 946f047..7c30f7c 100644
--- a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebugger.java
+++ b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebugger.java
@@ -50,6 +50,7 @@ import org.netbeans.api.debugger.DebuggerEngine;
 import org.netbeans.api.debugger.DebuggerInfo;
 import org.netbeans.api.debugger.DebuggerManager;
 import org.netbeans.api.debugger.DebuggerManagerAdapter;
+import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
 import org.netbeans.modules.cnd.debugger.gdb2.mi.MICommand;
 import org.netbeans.modules.cnd.debugger.gdb2.mi.MICommandInjector;
 import org.netbeans.modules.cnd.debugger.gdb2.mi.MIConst;
@@ -75,6 +76,7 @@ import org.openide.filesystems.FileUtil;
 import org.openide.text.Annotatable;
 import org.openide.text.Line;
 import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
 import org.openide.util.Pair;
 import org.openide.util.RequestProcessor;
 
@@ -811,15 +813,20 @@ public final class CPPLiteDebugger {
         CPPLiteDebugger debugger = es[0].lookupFirst(null, CPPLiteDebugger.class);
         List<String> executable = new ArrayList<>();
         executable.add(configuration.getDebugger());
-        executable.add("--interpreter=mi");
-        executable.add("--tty=" + pty.getSlaveName());
+        executable.add("--interpreter=mi");             // NOI18N
+        executable.add("--tty=" + pty.getSlaveName());  // NOI18N
         if (configuration.isAttach()) {
-            executable.add("-p");
+            executable.add("-p");                       // NOI18N
             executable.add(Long.toString(configuration.getAttachProcessId()));
         }
+        if (configuration.getExecutable().size() > 1) {
+            executable.add("--args");                   // NOI18N
+        }
         executable.addAll(configuration.getExecutable());
-        Process debuggee = new ProcessBuilder(executable).directory(configuration.getDirectory()).start();
-        new RequestProcessor(configuration.getDisplayName() + " (pty deallocator)").post(() -> {
+        ProcessBuilder processBuilder = new ProcessBuilder(executable);
+        setParameters(processBuilder, configuration);
+        Process debuggee = processBuilder.start();
+        new RequestProcessor(configuration.getDisplayName() + " (pty deallocator)").post(() -> {    // NOI18N
             try {
                 while (debuggee.isAlive()) {
                     try {
@@ -878,6 +885,25 @@ public final class CPPLiteDebugger {
         };
     }
 
+    private static void setParameters(ProcessBuilder processBuilder, CPPLiteDebuggerConfig configuration) {
+        ExplicitProcessParameters processParameters = configuration.getProcessParameters();
+        if (processParameters.getWorkingDirectory() != null) {
+            processBuilder.directory(processParameters.getWorkingDirectory());
+        }
+        if (!processParameters.getEnvironmentVariables().isEmpty()) {
+            Map<String, String> environment = processBuilder.environment();
+            for (Map.Entry<String, String> entry : processParameters.getEnvironmentVariables().entrySet()) {
+                String env = entry.getKey();
+                String val = entry.getValue();
+                if (val != null) {
+                    environment.put(env, val);
+                } else {
+                    environment.remove(env);
+                }
+            }
+        }
+    }
+
     private class BreakpointsHandler extends DebuggerManagerAdapter implements PropertyChangeListener {
 
         private final Map<String, CPPLiteBreakpoint> breakpointsById = new ConcurrentHashMap<>();
diff --git a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebuggerConfig.java b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebuggerConfig.java
index 3a8ac18..7ea6634 100644
--- a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebuggerConfig.java
+++ b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/CPPLiteDebuggerConfig.java
@@ -19,10 +19,12 @@
 
 package org.netbeans.modules.cpplite.debugger;
 
-import java.io.File;
+import java.util.ArrayList;
 import java.util.List;
 
 import org.netbeans.api.annotations.common.NullAllowed;
+import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
+import org.openide.util.Exceptions;
 
 /**
  *
@@ -31,16 +33,33 @@ import org.netbeans.api.annotations.common.NullAllowed;
 public final class CPPLiteDebuggerConfig {
 
     private final List<String> executable;
-    private final File directory;
+    private final ExplicitProcessParameters processParameters;
     @NullAllowed
     private final Long processId;
     private final String debugger;
 
-    public CPPLiteDebuggerConfig(List<String> executable, File directory, @NullAllowed Long processId, String debugger) {
-        this.executable = executable;
-        this.directory = directory;
+    public CPPLiteDebuggerConfig(List<String> executable, ExplicitProcessParameters processParameters, @NullAllowed Long processId, String debugger) {
+        this.processParameters = processParameters;
         this.processId = processId;
         this.debugger = debugger;
+        if (processParameters.isArgReplacement()) {
+            this.executable = new ArrayList<>();
+            this.executable.add(executable.get(0));
+            if (processParameters.getArguments() != null) {
+                this.executable.addAll(processParameters.getArguments());
+            }
+        } else {
+            if (processParameters.getArguments() != null) {
+                this.executable = new ArrayList<>();
+                this.executable.addAll(executable);
+                this.executable.addAll(processParameters.getArguments());
+            } else {
+                this.executable = executable;
+            }
+        }
+        if (processParameters.getLauncherArguments() != null) {
+            Exceptions.printStackTrace(new IllegalStateException("Launcher arguments " + processParameters.getLauncherArguments() + " can not be accepted by CPPLite debugger"));
+        }
     }
 
     public String getDisplayName() {
@@ -55,10 +74,10 @@ public final class CPPLiteDebuggerConfig {
     }
 
     /**
-     * Get the directory in which the executable command is to be launched.
+     * Get the parameters which the executable command is to be launched with.
      */
-    public File getDirectory() {
-        return directory;
+    public ExplicitProcessParameters getProcessParameters() {
+        return processParameters;
     }
 
     /**
diff --git a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/api/Debugger.java b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/api/Debugger.java
index fc8e741..ef98531 100644
--- a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/api/Debugger.java
+++ b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/api/Debugger.java
@@ -21,6 +21,7 @@ package org.netbeans.modules.cpplite.debugger.api;
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
 
 import org.netbeans.modules.cpplite.debugger.CPPLiteDebugger;
 import org.netbeans.modules.cpplite.debugger.CPPLiteDebuggerConfig;
@@ -38,8 +39,9 @@ public class Debugger {
 
     public static Process startInDebugger(List<String> command, File directory) throws IOException {
         CPPLiteDebugger[] debugger = new CPPLiteDebugger[] { null };
+        ExplicitProcessParameters processParameters = ExplicitProcessParameters.builder().workingDirectory(directory).build();
         Process engineProcess = CPPLiteDebugger.startDebugging(
-                new CPPLiteDebuggerConfig(command, directory, null, "gdb"),
+                new CPPLiteDebuggerConfig(command, processParameters, null, "gdb"),
                 engine -> {
                     debugger[0] = engine.lookupFirst(null, CPPLiteDebugger.class);
                 });
diff --git a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/ni/NIDebuggerProviderImpl.java b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/ni/NIDebuggerProviderImpl.java
index 7d71aaf..552107e 100644
--- a/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/ni/NIDebuggerProviderImpl.java
+++ b/cpplite/cpplite.debugger/src/org/netbeans/modules/cpplite/debugger/ni/NIDebuggerProviderImpl.java
@@ -31,6 +31,7 @@ import org.netbeans.api.debugger.Breakpoint;
 import org.netbeans.api.debugger.DebuggerEngine;
 import org.netbeans.api.extexecution.ExecutionDescriptor;
 import org.netbeans.api.extexecution.ExecutionService;
+import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
 import org.netbeans.modules.cpplite.debugger.CPPFrame;
 import org.netbeans.modules.cpplite.debugger.CPPLiteDebugger;
 import org.netbeans.modules.cpplite.debugger.CPPLiteDebuggerConfig;
@@ -42,12 +43,14 @@ import org.netbeans.modules.nativeimage.api.debug.EvaluateException;
 import org.netbeans.modules.nativeimage.api.debug.NIFrame;
 import org.netbeans.modules.nativeimage.api.debug.NILineBreakpointDescriptor;
 import org.netbeans.modules.nativeimage.api.debug.NIVariable;
+import org.netbeans.modules.nativeimage.api.debug.StartDebugParameters;
 import org.netbeans.modules.nativeimage.spi.debug.NIDebuggerProvider;
 import org.netbeans.modules.nativeimage.spi.debug.filters.FrameDisplayer;
 
 import org.openide.LifecycleManager;
 import org.openide.util.RequestProcessor;
 import org.netbeans.modules.nativeimage.spi.debug.filters.VariableDisplayer;
+import org.openide.util.Lookup;
 
 /**
  *
@@ -85,10 +88,20 @@ public class NIDebuggerProviderImpl implements NIDebuggerProvider {
     }
 
     @Override
-    public CompletableFuture<Void> start(List<String> command, File workingDirectory, String miDebugger, String displayName, ExecutionDescriptor executionDescriptor, Consumer<DebuggerEngine> startedEngine) {
+    public CompletableFuture<Void> start(StartDebugParameters debugParameters, Consumer<DebuggerEngine> startedEngine) {
         if (debugger != null) {
             throw new IllegalStateException("Debugger has started already.");
         }
+        List<String> command = debugParameters.getCommand();
+        String miDebugger = debugParameters.getDebugger();
+        String displayName = debugParameters.getDisplayName();
+        ExecutionDescriptor executionDescriptor = debugParameters.getExecutionDescriptor();
+        Lookup contextLookup = debugParameters.getContextLookup();
+        ExplicitProcessParameters explicitParameters = contextLookup != null ? ExplicitProcessParameters.buildExplicitParameters(contextLookup) : null;
+        if (explicitParameters == null) {
+            explicitParameters = ExplicitProcessParameters.builder().workingDirectory(debugParameters.getWorkingDirectory()).build();
+        }
+        final ExplicitProcessParameters processParameters = explicitParameters;
         if (executionDescriptor == null) {
             executionDescriptor = new ExecutionDescriptor()
                 .showProgress(true)
@@ -104,7 +117,7 @@ public class NIDebuggerProviderImpl implements NIDebuggerProvider {
             try {
                 LifecycleManager.getDefault().saveAll();
                 engineProcess = CPPLiteDebugger.startDebugging(
-                        new CPPLiteDebuggerConfig(command, workingDirectory, null, miDebugger),
+                        new CPPLiteDebuggerConfig(command, processParameters, null, miDebugger),
                         engine -> {
                             debugger[0] = engine.lookupFirst(null, CPPLiteDebugger.class);
                             this.debugger = debugger[0];
@@ -147,10 +160,11 @@ public class NIDebuggerProviderImpl implements NIDebuggerProvider {
         if (debugger != null) {
             throw new IllegalStateException("Debugger has started already.");
         }
+        ExplicitProcessParameters processParameters = ExplicitProcessParameters.builder().workingDirectory(new File(System.getProperty("user.dir", ""))).build();   // NOI18N
         CompletableFuture<Void> completed = new CompletableFuture<>();
         try {
             CPPLiteDebugger.startDebugging(
-                    new CPPLiteDebuggerConfig(Collections.singletonList(executablePath), new File(System.getProperty("user.dir")), processId, miDebugger),
+                    new CPPLiteDebuggerConfig(Collections.singletonList(executablePath), processParameters, processId, miDebugger),
                     engine -> {
                         CPPLiteDebugger debugger = engine.lookupFirst(null, CPPLiteDebugger.class);
                         this.debugger = debugger;
diff --git a/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/AbstractDebugTest.java b/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/AbstractDebugTest.java
index d3aca0c..26aff70 100644
--- a/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/AbstractDebugTest.java
+++ b/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/AbstractDebugTest.java
@@ -18,21 +18,34 @@
  */
 package org.netbeans.modules.cpplite.debugger;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.StringWriter;
+import java.io.Writer;
 import java.net.URI;
 import java.util.Arrays;
+import java.util.List;
 import junit.framework.Test;
 
 import org.netbeans.api.debugger.DebuggerEngine;
+import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
 import org.netbeans.junit.NbModuleSuite;
 import org.netbeans.junit.NbTestCase;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
 
 public abstract class AbstractDebugTest extends NbTestCase {
 
     protected DebuggerEngine engine;
     protected CPPLiteDebugger debugger;
     protected Process process;
+    protected StringBuffer stdOut;
+    protected StringBuffer stdErr;
 
     private final int[] suspendCount = new int[]{0};
     private final int[] resumeCount = new int[]{0};
@@ -41,6 +54,14 @@ public abstract class AbstractDebugTest extends NbTestCase {
         super(s);
     }
 
+    protected final static void createSourceFile(String fileName, File wd, String content) throws IOException {
+        FileObject source = FileUtil.createData(FileUtil.toFileObject(wd), "main.cpp");
+        try (OutputStream os = source.getOutputStream();
+            Writer w = new OutputStreamWriter(os)) {
+            w.append(content);
+        }
+    }
+
     protected final static void compileC(String name, File wd) throws IOException, InterruptedException {
         Process compile = new ProcessBuilder("gcc", "-o", name, "-g", name + ".c").directory(wd).start();
         assertEquals(0, compile.waitFor());
@@ -52,9 +73,20 @@ public abstract class AbstractDebugTest extends NbTestCase {
     }
 
     protected final void startDebugging(String name, File wd) throws IOException {
+        ExplicitProcessParameters processParameters = ExplicitProcessParameters.builder().workingDirectory(wd).build();
+        startDebugging(name, wd, processParameters);
+    }
+
+    protected final void startDebugging(String name, File wd, ExplicitProcessParameters processParameters) throws IOException {
+        startDebugging(Arrays.asList(new File(wd, name).getAbsolutePath()), processParameters);
+    }
+
+    protected final void startDebugging(List<String> executable, ExplicitProcessParameters processParameters) throws IOException {
         this.process = CPPLiteDebugger.startDebugging(
-                new CPPLiteDebuggerConfig(Arrays.asList(new File(wd, name).getAbsolutePath()), wd, null, "gdb"),
+                new CPPLiteDebuggerConfig(executable, processParameters, null, "gdb"),
                 engine -> this.engine = engine);
+        stdOut = outputFrom(process.getInputStream());
+        stdErr = outputFrom(process.getErrorStream());
         debugger = engine.lookupFirst(null, CPPLiteDebugger.class);
         debugger.addStateListener(new CPPLiteDebugger.StateListener() {
             @Override
@@ -120,4 +152,23 @@ public abstract class AbstractDebugTest extends NbTestCase {
     public static Test suite() {
         return NbModuleSuite.emptyConfiguration().gui(false).suite();
     }
+
+    private static StringBuffer outputFrom(InputStream inputStream) {
+        StringBuffer buffer = new StringBuffer();
+        new Thread("Process output") {
+            @Override
+            public void run() {
+                try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        buffer.append(line);
+                        buffer.append('\n');
+                    }
+                } catch (IOException ex) {
+                    buffer.append(ex);
+                }
+            }
+        }.start();
+        return buffer;
+    }
 }
diff --git a/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/ProcessParametersTest.java b/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/ProcessParametersTest.java
new file mode 100644
index 0000000..fd5eb18
--- /dev/null
+++ b/cpplite/cpplite.debugger/test/unit/src/org/netbeans/modules/cpplite/debugger/ProcessParametersTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.netbeans.modules.cpplite.debugger;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import junit.framework.Test;
+import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
+import org.netbeans.junit.NbModuleSuite;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+
+/**
+ * Tests application parameters set through {@link ExplicitProcessParameters}.
+ *
+ * @author Martin Entlicher
+ */
+public class ProcessParametersTest extends AbstractDebugTest {
+
+    private static final String CPP_APP =
+            "#include <unistd.h>\n" +
+            "#include <stdio.h>\n" +
+            "#include <limits.h>\n" +
+            "\n" +
+            "int main(int argc, char **argv, char **envp) {\n" +
+            "   char buffer[PATH_MAX];\n" +
+            "   if (getcwd(buffer, sizeof(buffer)) != NULL) {\n" +
+            "       printf(\"CWD:%s\\n\", buffer);\n" +
+            "   } else {\n" +
+            "       perror(\"getcwd() error\");\n" +
+            "       return 1;\n" +
+            "   }\n" +
+            "   while (--argc) {\n" +
+            "      printf(\"ARG:%s\\n\", *(++argv));\n" +
+            "   }\n" +
+            "   for (char **e = envp; *e; e++) {\n" +
+            "      printf(\"ENV:%s\\n\", *e);\n" +
+            "   }\n" +
+            "   return 0;\n" +
+            "}";
+
+    public ProcessParametersTest(String s) {
+        super(s);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        clearWorkDir();
+    }
+
+    public void testWorkingDir() throws Exception {
+        File wd = getWorkDir();
+        createSourceFile("main.cpp", wd, CPP_APP);
+        compileCPP("main", wd);
+        File appWD = Files.createTempDirectory("TestCWD").toFile();
+        try {
+            ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                    workingDirectory(appWD).
+                    build();
+            startDebugging("main", wd, params);
+            assertEquals(0, waitAppProcessExit());
+            String output = stdOut.toString();
+            assertEquals("CWD:" + appWD.getAbsolutePath(), output.substring(0, output.indexOf('\n')));
+        } finally {
+            appWD.delete();
+        }
+    }
+
+    public void testArgumentsOwn() throws Exception {
+        File wd = getWorkDir();
+        createSourceFile("main.cpp", wd, CPP_APP);
+        compileCPP("main", wd);
+        ExplicitProcessParameters params = ExplicitProcessParameters.empty();
+        startDebugging(Arrays.asList(new File(wd, "main").getAbsolutePath(), "Param1", "Param 2"), params);
+        assertEquals(0, waitAppProcessExit());
+        assertArguments("Param1", "Param 2");
+    }
+
+    public void testArgumentsExplicit() throws Exception {
+        File wd = getWorkDir();
+        createSourceFile("main.cpp", wd, CPP_APP);
+        compileCPP("main", wd);
+        ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                arg("ExpParam1").
+                arg("ExpParam 2").
+                build();
+        startDebugging(Arrays.asList(new File(wd, "main").getAbsolutePath()), params);
+        assertEquals(0, waitAppProcessExit());
+        assertArguments("ExpParam1", "ExpParam 2");
+    }
+
+    public void testArgumentsCombinedReplace() throws Exception {
+        File wd = getWorkDir();
+        createSourceFile("main.cpp", wd, CPP_APP);
+        compileCPP("main", wd);
+        ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                arg("ExpParam1").
+                arg("ExpParam 2").
+                build();
+        startDebugging(Arrays.asList(new File(wd, "main").getAbsolutePath(), "Param1", "Param 2"), params);
+        assertEquals(0, waitAppProcessExit());
+        assertArguments("ExpParam1", "ExpParam 2");
+    }
+
+    public void testArgumentsCombinedAppend() throws Exception {
+        File wd = getWorkDir();
+        createSourceFile("main.cpp", wd, CPP_APP);
+        compileCPP("main", wd);
+        ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                arg("ExpParam1").
+                arg("ExpParam 2").
+                replaceArgs(false).
+                build();
+        startDebugging(Arrays.asList(new File(wd, "main").getAbsolutePath(), "Param1", "Param 2"), params);
+        assertEquals(0, waitAppProcessExit());
+        assertArguments("Param1", "Param 2", "ExpParam1", "ExpParam 2");
+    }
+
+    public void testEnvironmentVariables() throws Exception {
+        File wd = getWorkDir();
+        createSourceFile("main.cpp", wd, CPP_APP);
+        compileCPP("main", wd);
+        ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                environmentVariable("TEST_VAR1", "test variable value 1").
+                environmentVariable("TEST_VAR2", "test variable value 2").
+                environmentVariable("USER", null).
+                build();
+        startDebugging("main", wd, params);
+        assertEquals(0, waitAppProcessExit());
+        String output = stdOut.toString();
+        Map<String, String> env = new HashMap<>();
+        String[] lines = output.split("\n");
+        for (String line : lines) {
+            if (line.startsWith("ENV:")) {
+                line = line.substring("ENV:".length());
+                String[] var = line.split("=");
+                env.put(var[0], var[1]);
+            }
+        }
+        assertEquals("test variable value 1", env.get("TEST_VAR1"));
+        assertEquals("test variable value 2", env.get("TEST_VAR2"));
+        assertFalse("Removed variable is present", env.containsKey("USER"));
+    }
+
+    public static Test suite() {
+        return NbModuleSuite.emptyConfiguration()
+                            .addTest(ProcessParametersTest.class)
+                            .enableModules(".*", ".*")
+                            .gui(false)
+                            .suite();
+    }
+
+    private void assertArguments(String... args) {
+        String output = stdOut.toString();
+        int index1 = 0;
+        int index2 = 0;
+        for (int i = 0; i < args.length; i++) {
+            index1 = output.indexOf("ARG:", index2);
+            if (index1 < 0) {
+                fail("Missing argument " + i + " : " + args[i]);
+            }
+            index2 = output.indexOf('\n', index1);
+            assertEquals("ARG:" + args[i] , output.substring(index1, index2));
+        }
+        index1 = output.indexOf("ARG:", index2);
+        if (index1 >= 0) {
+            fail("An extra argument: " + output.substring(index1));
+        }
+    }
+}
diff --git a/extide/gradle/apichanges.xml b/extide/gradle/apichanges.xml
index b21982e..a9592af 100644
--- a/extide/gradle/apichanges.xml
+++ b/extide/gradle/apichanges.xml
@@ -83,6 +83,23 @@ is the proper place.
     <!-- ACTUAL CHANGES BEGIN HERE: -->
 
     <changes>
+        <change id="tooling-runWorkingDir-runEnvironment">
+            <api name="general"/>
+            <summary>NetBeans Tooling plugin recognizes "runWorkingDir" and "runEnvironment" properties.</summary>
+            <version major="2" minor="21"/>
+            <date day="23" month="9" year="2021"/>
+            <author login="entlicher"/>
+            <compatibility semantic="compatible" addition="yes"/>
+            <description>
+                Introduced <code>runWorkingDir</code> and <code>runEnvironment</code> project properties that can be passed
+                to NetBeans Tooling plugin in order to inject user parameters into gradle JavaExec task configurations.
+                The property <code>runWorkingDir</code> shall contain an absolute path of the working directory.
+                The property <code>runEnvironment</code> shall contain possibly quoted space-separated expressions of
+                <code>VAR_NAME=VAR_VALUE</code>, or <code>!VAR_NAME</code>. The <code>VAR_NAME=VAR_VALUE</code> sets
+                environment variable <code>VAR_NAME</code> to value <code>VAR_VALUE</code> and <code>!VAR_NAME</code>
+                removes variable <code>VAR_NAME</code> from the set of environment variables.
+            </description>
+        </change>
         <change id="gradle-init">
             <api name="general"/>
             <summary>
diff --git a/extide/gradle/arch.xml b/extide/gradle/arch.xml
index 1c686f1..78d3e8f 100644
--- a/extide/gradle/arch.xml
+++ b/extide/gradle/arch.xml
@@ -156,6 +156,20 @@
          If passing more arguments, the whole -P must be properly quoted. Arguments that contain spaces must
          also use quoting or escapes.
      </api>
+     <api category="friend" group="property" name="runWorkingDir" type="export">
+         NetBeans Tooling plugin recognizes <code>runWorkingDir</code> project property, and injects
+         specified working directory into JavaExec executing the user code. Use <code>-PrunWorkingDir=...</code>.
+         If the working directory contain spaces, it must also use quoting or escapes.
+     </api>
+     <api category="friend" group="property" name="runEnvironment" type="export">
+         NetBeans Tooling plugin recognizes <code>runEnvironment</code> project property, and injects
+         specified environmental variables into JavaExec executing the user code. Use <code>-PrunEnvironment=...</code>.
+         The value consists of expressions of <code>VAR_NAME=VAR_VALUE</code>, or <code>!VAR_NAME</code>.
+         The <code>VAR_NAME=VAR_VALUE</code> sets environment variable <code>VAR_NAME</code> to value <code>VAR_VALUE</code>
+         and <code>!VAR_NAME</code> removes variable <code>VAR_NAME</code> from the set of environment variables.
+         If passing more arguments, the whole -P must be properly quoted. Variables whose values contain spaces must
+         also use quoting or escapes.
+     </api>
  </answer>
 
  <answer id="resources-file">
diff --git a/extide/gradle/netbeans-gradle-tooling/src/main/groovy/org/netbeans/modules/gradle/tooling/NetBeansRunSinglePlugin.groovy b/extide/gradle/netbeans-gradle-tooling/src/main/groovy/org/netbeans/modules/gradle/tooling/NetBeansRunSinglePlugin.groovy
index 0663334..8470da8 100644
--- a/extide/gradle/netbeans-gradle-tooling/src/main/groovy/org/netbeans/modules/gradle/tooling/NetBeansRunSinglePlugin.groovy
+++ b/extide/gradle/netbeans-gradle-tooling/src/main/groovy/org/netbeans/modules/gradle/tooling/NetBeansRunSinglePlugin.groovy
@@ -35,6 +35,8 @@ class NetBeansRunSinglePlugin implements Plugin<Project> {
     static String RUN_SINGLE_MAIN = "runClassName"
     static String RUN_SINGLE_ARGS = "runArgs"
     static String RUN_SINGLE_JVM_ARGS = "runJvmArgs"
+    static String RUN_SINGLE_CWD = "runWorkingDir"
+    static String RUN_SINGLE_ENV = "runEnvironment"
     
     void apply(Project project) {
         project.afterEvaluate {
@@ -65,7 +67,107 @@ class NetBeansRunSinglePlugin implements Plugin<Project> {
             if (project.hasProperty(RUN_SINGLE_ARGS)) {
                 args = project.getProperty(RUN_SINGLE_ARGS).tokenize(' ')
             }
+            if (project.hasProperty(RUN_SINGLE_CWD)) {
+                workingDir = project.getProperty(RUN_SINGLE_CWD)
+            }
+            if (project.hasProperty(RUN_SINGLE_ENV)) {
+                // Quoted space-separated expressions of <ENV_VAR>=<ENV_VALUE>
+                // to set environment variables, or !<ENV_VAR>
+                // to remove environment variables
+                unescapeParameters(project.getProperty(RUN_SINGLE_ENV)).each {
+                    env ->
+                        if (env.startsWith("!")) {
+                            environment.remove(env.substring(1))
+                        } else {
+                            def i = env.indexOf("=")
+                            if (i > 0) {
+                                environment[env.substring(0, i)] = env.substring(i + 1)
+                            }
+                        }
+                }
+            }
         }
     }
-    
+
+    private String[] unescapeParameters(String s) {
+        final int NULL = 0x0;
+        final int IN_PARAM = 0x1;
+        final int IN_DOUBLE_QUOTE = 0x2;
+        final int IN_SINGLE_QUOTE = 0x3;
+        ArrayList<String> params = new ArrayList<>(5);
+        char c;
+
+        int state = NULL;
+        StringBuilder buff = new StringBuilder(20);
+        int slength = s.length();
+
+        for (int i = 0; i < slength; i++) {
+            c = s.charAt(i);
+            switch (state) {
+                case NULL:
+                    switch (c) {
+                        case '\'':
+                            state = IN_SINGLE_QUOTE;
+                            break;
+                        case '"':
+                            state = IN_DOUBLE_QUOTE;
+                            break;
+                        default:
+                            if (!Character.isWhitespace(c)) {
+                                buff.append(c);
+                                state = IN_PARAM;
+                            }
+                    }
+                    break;
+                case IN_SINGLE_QUOTE:
+                    if (c != '\'') {
+                        buff.append(c);
+                    } else {
+                        state = IN_PARAM;
+                    }
+                    break;
+                case IN_DOUBLE_QUOTE:
+                    switch (c) {
+                        case '\\':
+                            char peek = (i < slength - 1) ? s.charAt(i+1) : Character.MIN_VALUE;
+                            if (peek == '"' || peek =='\\') {
+                                buff.append(peek);
+                                i++;
+                            } else {
+                                buff.append(c);
+                            }
+                            break;
+                        case '"':
+                            state = IN_PARAM;
+                            break;
+                        default:
+                            buff.append(c);
+                    }
+                    break;
+                case IN_PARAM:
+                    switch (c) {
+                        case '\'':
+                            state = IN_SINGLE_QUOTE;
+                            break;
+                        case '"':
+                            state = IN_DOUBLE_QUOTE;
+                            break;
+                        default:
+                          if (Character.isWhitespace(c)) {
+                              params.add(buff.toString());
+                              buff.setLength(0);
+                              state = NULL;
+                          } else {
+                              buff.append(c);
+                          }
+                    }
+                    break;
+            }
+        }
+        if (buff.length() > 0) {
+            params.add(buff.toString());
+        }
+
+        return params.toArray(new String[params.size()])
+    }
 }
diff --git a/ide/extexecution.base/src/org/netbeans/api/extexecution/base/ExplicitProcessParameters.java b/ide/extexecution.base/src/org/netbeans/api/extexecution/base/ExplicitProcessParameters.java
index 9aa9d03..a25e199 100644
--- a/ide/extexecution.base/src/org/netbeans/api/extexecution/base/ExplicitProcessParameters.java
+++ b/ide/extexecution.base/src/org/netbeans/api/extexecution/base/ExplicitProcessParameters.java
@@ -18,11 +18,15 @@
  */
 package org.netbeans.api.extexecution.base;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import org.netbeans.api.annotations.common.CheckForNull;
 import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.api.annotations.common.NullAllowed;
 import org.openide.util.Lookup;
@@ -45,8 +49,7 @@ import org.openide.util.Lookup;
  * process arguments.
  * <div class="nonnormative">
  * For <b>java applications</b> when {@code java} executable is used to launch the application, or even Maven project (see below), the <b>launcherArgs</b> should correspond to VM
- * arguments, and <b>args</b> correspond to the main class' arguments (passed to the main class). Environment variables for the new process are not 
- * supported at the moment.
+ * arguments, and <b>args</b> correspond to the main class' arguments (passed to the main class). Additional environment variables can be specified.
  * </div>
  * <p>
  * If the object is marked as {@link #isArgReplacement()}, the launcher implementor SHOULD replace all
@@ -92,17 +95,22 @@ public final class ExplicitProcessParameters {
     private final List<String>    arguments;
     private final boolean  replaceArgs;
     private final boolean  replaceLauncherArgs;
+    private final File workingDirectory;
+    private final Map<String, String> environmentVars;
 
     private ExplicitProcessParameters(int position, List<String> launcherArguments, 
-            List<String> arguments, boolean appendArgs, boolean appendLauncherArgs) {
+            List<String> arguments, boolean appendArgs, boolean appendLauncherArgs,
+            File workingDirectory, Map<String, String> environmentVars) {
         this.position = position;
         this.launcherArguments = launcherArguments == null ? null : Collections.unmodifiableList(launcherArguments);
         this.arguments = arguments == null ? null : Collections.unmodifiableList(arguments);
         this.replaceArgs = appendArgs;
         this.replaceLauncherArgs = appendLauncherArgs;
+        this.workingDirectory = workingDirectory;
+        this.environmentVars = environmentVars == null ? null : Collections.unmodifiableMap(environmentVars);
     }
     
-    private static final ExplicitProcessParameters EMPTY = new ExplicitProcessParameters(0, null, null, false, false);
+    private static final ExplicitProcessParameters EMPTY = new ExplicitProcessParameters(0, null, null, false, false, null, null);
     
     /**
      * Returns an empty instance of parameters that has no effect. DO NOT check for emptiness by
@@ -122,7 +130,10 @@ public final class ExplicitProcessParameters {
         if (isArgReplacement() || isLauncherArgReplacement()) {
             return false;
         }
-        return (((arguments == null) || arguments.isEmpty()) && (launcherArguments == null || launcherArguments.isEmpty()));
+        return  ((arguments == null) || arguments.isEmpty()) &&
+                (launcherArguments == null || launcherArguments.isEmpty()) &&
+                workingDirectory == null &&
+                (environmentVars == null || environmentVars.isEmpty());
     }
 
     /**
@@ -190,6 +201,29 @@ public final class ExplicitProcessParameters {
     }
 
     /**
+     * Returns working directory to be set for the process.
+     *
+     * @return working directory, or <code>nul</code>
+     * @since 1.20
+     */
+    public @CheckForNull File getWorkingDirectory() {
+        return workingDirectory;
+    }
+
+    /**
+     * Returns a map of additional environment variables to be set for the process.
+     * Always non-null. Values of existing environment variables are overridden.
+     * A <code>null</code> value of a variable should be interpreted as a removal
+     * of that variable from the environment.
+     *
+     * @return map of additional environment variables
+     * @since 1.20
+     */
+    public @NonNull Map<String, String> getEnvironmentVariables() {
+        return environmentVars != null ? environmentVars : Collections.emptyMap();
+    }
+
+    /**
      * Merges ExplicitProcessParameters instructions found in the Lookup. See {@link #buildExplicitParameters(java.util.Collection)}
      * for more details.
      * @param context context for the execution
@@ -214,6 +248,8 @@ public final class ExplicitProcessParameters {
      * <i>Note:</i> if a replacement instruction and all the following (if any) have {@link #getArguments()} {@code null} (= no change), 
      * the result will report <b>no change</b>. It is therefore possible to <b>discard all contributions</b> by appending a no-change replacement 
      * last.
+     * <p>
+     * Environment variables are overridden by newly set variables.
      * 
      * @param items individual instructions.
      * @return combined instructions.
@@ -238,7 +274,7 @@ public final class ExplicitProcessParameters {
      * <li><b>appends</b> launcher arguments
      * <li><b>replaces</b> (normal) arguments
      * </ul>
-     * and the mode can be overriden for each group.
+     * and the mode can be overridden for each group.
      */
     public final static class Builder {
         private int position = 0;
@@ -246,6 +282,8 @@ public final class ExplicitProcessParameters {
         private List<String> arguments = null;
         private Boolean  replaceArgs;
         private Boolean  replaceLauncherArgs;
+        private File workingDirectory = null;
+        private Map<String, String> environmentVars;
         
         private void initArgs() {
             if (arguments == null) {
@@ -371,6 +409,54 @@ public final class ExplicitProcessParameters {
         }
 
         /**
+         * Sets working directory to be used for the process.
+         *
+         * @param workingDirectory the working directory
+         * @return the builder
+         * @since 1.20
+         */
+        public Builder workingDirectory(File workingDirectory) {
+            this.workingDirectory = workingDirectory;
+            return this;
+        }
+
+        /**
+         * Provide additional environment variables for the process. Values of
+         * existing environment variables are overridden. <code>null</code> values
+         * are interpreted as removal of the respective variables from the environment.
+         *
+         * @param env a map of additional environment variables
+         * @return the builder
+         * @since 1.20
+         */
+        public Builder environmentVariables(Map<String, String> env) {
+            if (!env.isEmpty()) {
+                if (this.environmentVars == null) {
+                    this.environmentVars = new HashMap<>();
+                }
+                this.environmentVars.putAll(env);
+            }
+            return this;
+        }
+
+        /**
+         * Provide an additional environment variable for the process. If the variable
+         * already exists, it's overridden with the new value.
+         *
+         * @param name name of the environment variable
+         * @param value value of the environment variable, or <code>null</code> in which case an existing variable is to be removed.
+         * @return the builder
+         * @since 1.20
+         */
+        public Builder environmentVariable(String name, String value) {
+            if (this.environmentVars == null) {
+                this.environmentVars = new HashMap<>();
+            }
+            this.environmentVars.put(name, value);
+            return this;
+        }
+
+        /**
          * Defines a position for combining. The default rank is {@code 0}. When used in a collection in
          * {@link ExplicitProcessParameters#buildExplicitParameters(java.util.Collection)}, instances are sorted
          * by their position, in ascending order (lowest first).
@@ -416,6 +502,12 @@ public final class ExplicitProcessParameters {
             if (p.getArguments() != null) {
                 args(p.getArguments());
             }
+            if (p.getWorkingDirectory() != null) {
+                workingDirectory(p.getWorkingDirectory());
+            }
+            if (!p.getEnvironmentVariables().isEmpty()) {
+                environmentVariables(p.getEnvironmentVariables());
+            }
             return this;
         }
         
@@ -430,7 +522,7 @@ public final class ExplicitProcessParameters {
             return new ExplicitProcessParameters(position, launcherArguments, arguments, 
                     // if no args / launcher args given and no explicit instruction on append,
                     // make the args appending.
-                    aa, apa);
+                    aa, apa, workingDirectory, environmentVars);
         }
     }
 }
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/EvaluateException.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/EvaluateException.java
index 1d542bb..88a2cd8 100644
--- a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/EvaluateException.java
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/EvaluateException.java
@@ -21,7 +21,7 @@ package org.netbeans.modules.nativeimage.api.debug;
 /**
  * Thrown when evaluation in the native debugger fails.
  *
- * @since 1.0
+ * @since 0.1
  */
 public class EvaluateException extends Exception {
 
@@ -30,7 +30,7 @@ public class EvaluateException extends Exception {
      * specified detail message.
      *
      * @param msg the detail message.
-     * @since 1.0
+     * @since 0.1
      */
     public EvaluateException(String msg) {
         super(msg);
@@ -41,10 +41,9 @@ public class EvaluateException extends Exception {
      * existing exception.
      *
      * @param t exception.
-     * @since 1.0
+     * @since 0.1
      */
     public EvaluateException(Throwable t) {
         super(t.getLocalizedMessage(), t);
-        
     }
 }
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIDebugger.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIDebugger.java
index bdd0245..a66b353 100644
--- a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIDebugger.java
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIDebugger.java
@@ -21,6 +21,7 @@ package org.netbeans.modules.nativeimage.api.debug;
 import java.io.File;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
@@ -42,7 +43,7 @@ import org.openide.util.NbBundle;
 
 /**
  * Representation of a native image debugger.
- * @since 1.0
+ * @since 0.1
  */
 public final class NIDebugger {
 
@@ -59,7 +60,7 @@ public final class NIDebugger {
      * @throws IllegalStateException when the native debugger is not available
      *         (there is not an implementation of {@link NIDebuggerServiceProvider}
      *         registered in the default lookup).
-     * @since 1.0
+     * @since 0.1
      */
     @NbBundle.Messages({"MSG_NoNativeDebug=No native debugger is available. Please install native debugger module."})
     public static Builder newBuilder() throws IllegalStateException {
@@ -79,7 +80,7 @@ public final class NIDebugger {
      * @param id a unique ID of the breakpoint
      * @param breakpointDescriptor the breakpoint descriptor
      * @return an instance of the native breakpoint
-     * @since 1.0
+     * @since 0.1
      */
     public Breakpoint addLineBreakpoint(Object id, NILineBreakpointDescriptor breakpointDescriptor) {
         Breakpoint breakpoint = provider.addLineBreakpoint(id, breakpointDescriptor);
@@ -91,7 +92,7 @@ public final class NIDebugger {
      * Remove breakpoint with the given id.
      *
      * @param id the ID of the breakpoint to remove
-     * @since 1.0
+     * @since 0.1
      */
     public void removeBreakpoint(Object id) {
         provider.removeBreakpoint(id);
@@ -107,13 +108,42 @@ public final class NIDebugger {
      * @param executionDescriptor execution descriptor that describes the runtime attributes
      * @param startedEngine the corresponding DebuggerEngine is passed to this consumer
      * @return future that completes on the execution finish
-     * @since 1.0
+     * @since 0.1
+     * @deprecated Use {@link #start(org.netbeans.modules.nativeimage.api.debug.StartDebugParameters, java.util.function.Consumer)}
      */
+    @Deprecated
     public CompletableFuture<Void> start(List<String> command, File workingDirectory, String debugger, String displayName, ExecutionDescriptor executionDescriptor, Consumer<DebuggerEngine> startedEngine) {
         return provider.start(command, workingDirectory, debugger, displayName, executionDescriptor, startedEngine);
     }
 
     /**
+     * Start the actual debugging session. Call this typically after breakpoints are added.
+     *
+     * @param debugParameters parameters to start the debugging with
+     * @param startedEngine the corresponding DebuggerEngine is passed to this consumer
+     * @return a future which is completed when the started debugger session finishes
+     * @since 0.5
+     */
+    public CompletableFuture<Void> start(StartDebugParameters debugParameters, Consumer<DebuggerEngine> startedEngine) {
+        Objects.requireNonNull(debugParameters);
+        CompletableFuture<Void> start = provider.start(debugParameters, startedEngine);
+        if (start.isCompletedExceptionally()) {
+            try {
+                start.get();
+            } catch (ExecutionException ex) {
+                if (ex.getCause() instanceof UnsupportedOperationException) {
+                    return startLegacy(debugParameters, startedEngine);
+                }
+            } catch (Throwable ex) {}
+        }
+        return start;
+    }
+
+    private CompletableFuture<Void> startLegacy(StartDebugParameters debugParameters, Consumer<DebuggerEngine> startedEngine) {
+        return provider.start(debugParameters.getCommand(), debugParameters.getWorkingDirectory(), debugParameters.getDebugger(), debugParameters.getDisplayName(), debugParameters.getExecutionDescriptor(), startedEngine);
+    }
+
+    /**
      * Attach to a process and create a debugging session. Call this typically after breakpoints are added.
      *
      * @param executablePath path to an executable representing the native image
@@ -135,7 +165,7 @@ public final class NIDebugger {
      *                   when <code>null</code> the expression is used as the name
      * @param frame the frame to evaluate at
      * @return the completable future with the evaluation result
-     * @since 1.0
+     * @since 0.1
      */
     public CompletableFuture<NIVariable> evaluateAsync(String expression, String resultName, NIFrame frame) {
         return provider.evaluateAsync(expression, resultName, frame);
@@ -151,7 +181,7 @@ public final class NIDebugger {
      * @param frame the frame to evaluate at
      * @return the evaluation result
      * @throws EvaluateException when evaluation fails
-     * @since 1.0
+     * @since 0.1
      */
     public NIVariable evaluate(String expression, String resultName, NIFrame frame) throws EvaluateException {
         try {
@@ -169,7 +199,7 @@ public final class NIDebugger {
      * @param length number of bytes to read
      * @return hexadecimal representation of the memory content, or <code>null</code>
      *         when the read is not successful
-     * @since 1.0
+     * @since 0.1
      */
     public String readMemory(String address, long offset, int length) {
         return provider.readMemory(address, offset, length);
@@ -178,7 +208,7 @@ public final class NIDebugger {
     /**
      * Get version of the underlying native debugger.
      *
-     * @since 1.0
+     * @since 0.1
      */
     public String getVersion() {
         return provider.getVersion();
@@ -230,7 +260,7 @@ public final class NIDebugger {
     /**
      * A builder that creates a Native Image debugger with optional displayers.
      *
-     * @since 1.0
+     * @since 0.1
      */
     public static final class Builder {
 
@@ -244,7 +274,7 @@ public final class NIDebugger {
          * Displayer of native frames.
          *
          * @param frameDisplayer translator of the native frame to it's displayed information
-         * @since 1.0
+         * @since 0.1
          */
         public Builder frameDisplayer(FrameDisplayer frameDisplayer) {
             this.debuggerProvider.setFrameDisplayer(frameDisplayer);
@@ -255,7 +285,7 @@ public final class NIDebugger {
          * Displayer of native variables.
          *
          * @param variablesDisplayer translator of native variables to displayed variables.
-         * @since 1.0
+         * @since 0.1
          */
         public Builder variablesDisplayer(VariableDisplayer variablesDisplayer) {
             this.debuggerProvider.setVariablesDisplayer(variablesDisplayer);
@@ -264,7 +294,7 @@ public final class NIDebugger {
 
         /**
          * Create the debugger instance.
-         * @since 1.0
+         * @since 0.1
          */
         public NIDebugger build() {
             return new NIDebugger(debuggerProvider);
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIFrame.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIFrame.java
index 4ffc48c..9cc338c 100644
--- a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIFrame.java
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIFrame.java
@@ -21,49 +21,49 @@ package org.netbeans.modules.nativeimage.api.debug;
 /**
  * Representation of a native stack frame.
  *
- * @since 1.0
+ * @since 0.1
  */
 public interface NIFrame {
 
     /**
      * Frame's thread ID.
-     * @since 1.0
+     * @since 0.1
      */
     String getThreadId();
 
     /**
      * Frame's depth level. The top frame has level 0.
-     * @since 1.0
+     * @since 0.1
      */
     int getLevel();
 
     /**
      * Frame's native address.
-     * @since 1.0
+     * @since 0.1
      */
     String getAddress();
 
     /**
      * A short name of the file associated with the frame.
-     * @since 1.0
+     * @since 0.1
      */
     String getShortFileName();
 
     /**
      * A full name of the file associated with the frame.
-     * @since 1.0
+     * @since 0.1
      */
     String getFullFileName();
 
     /**
      * Name of the function associated with the frame.
-     * @since 1.0
+     * @since 0.1
      */
     String getFunctionName();
 
     /**
      * 1-based line of the frame location.
-     * @since 1.0
+     * @since 0.1
      */
     int getLine();
 }
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NILineBreakpointDescriptor.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NILineBreakpointDescriptor.java
index 192be78..2033f39 100644
--- a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NILineBreakpointDescriptor.java
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NILineBreakpointDescriptor.java
@@ -23,7 +23,7 @@ import org.netbeans.api.annotations.common.CheckForNull;
 /**
  * Description of a line native breakpoint.
  *
- * @since 1.0
+ * @since 0.1
  */
 public final class NILineBreakpointDescriptor {
 
@@ -46,7 +46,7 @@ public final class NILineBreakpointDescriptor {
      *
      * @param filePath file path of the breakpoint
      * @param lineNumber 1-based line number
-     * @since 1.0
+     * @since 0.1
      */
     public static Builder newBuilder(String filePath, int lineNumber) {
         return new Builder(filePath, lineNumber);
@@ -55,7 +55,7 @@ public final class NILineBreakpointDescriptor {
     /**
      * Get path of the file.
      *
-     * @since 1.0
+     * @since 0.1
      */
     public String getFilePath() {
         return filePath;
@@ -64,7 +64,7 @@ public final class NILineBreakpointDescriptor {
     /**
      * Get 1-based line number.
      *
-     * @since 1.0
+     * @since 0.1
      */
     public int getLine() {
         return line;
@@ -73,7 +73,7 @@ public final class NILineBreakpointDescriptor {
     /**
      * Check if the breakpoint is to be enabled.
      *
-     * @since 1.0
+     * @since 0.1
      */
     public boolean isEnabled() {
         return enabled;
@@ -83,7 +83,7 @@ public final class NILineBreakpointDescriptor {
      * Get the breakpoint condition.
      *
      * @return the condition, or <code>null</code> when the breakpoint does not have any condition.
-     * @since 1.0
+     * @since 0.1
      */
     @CheckForNull
     public String getCondition() {
@@ -93,7 +93,7 @@ public final class NILineBreakpointDescriptor {
     /**
      * Check if the breakpoint is to be hidden (not user-visible).
      *
-     * @since 1.0
+     * @since 0.1
      */
     public boolean isHidden() {
         return hidden;
@@ -103,7 +103,7 @@ public final class NILineBreakpointDescriptor {
      * Builder of a line native breakpoint descriptor. The builder is reusable
      * and the built breakpoint descriptor can be used to update existing breakpoints.
      *
-     * @since 1.0
+     * @since 0.1
      */
     public static final class Builder {
 
@@ -121,7 +121,7 @@ public final class NILineBreakpointDescriptor {
         /**
          * Set a file path.
          *
-         * @since 1.0
+         * @since 0.1
          */
         public Builder filePath(String filePath) {
             this.filePath = filePath;
@@ -131,7 +131,7 @@ public final class NILineBreakpointDescriptor {
         /**
          * Set a 1-based line number.
          *
-         * @since 1.0
+         * @since 0.1
          */
         public Builder line(int lineNumber) {
             this.line = lineNumber;
@@ -141,7 +141,7 @@ public final class NILineBreakpointDescriptor {
         /**
          * Set a condition.
          *
-         * @since 1.0
+         * @since 0.1
          */
         public Builder condition(String condition) {
             this.condition = condition;
@@ -151,7 +151,7 @@ public final class NILineBreakpointDescriptor {
         /**
          * Set an enabled state of the breakpoint. The breakpoint is enabled by default.
          *
-         * @since 1.0
+         * @since 0.1
          */
         public Builder enabled(boolean enabled) {
             this.enabled = enabled;
@@ -162,7 +162,7 @@ public final class NILineBreakpointDescriptor {
          * Set a hidden state of the breakpoint. Hidden breakpoints are not visible
          * to users. The breakpoint is not hidden by default.
          *
-         * @since 1.0
+         * @since 0.1
          */
         public Builder hidden(boolean hidden) {
             this.hidden = hidden;
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIVariable.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIVariable.java
index cd46e7c..d200192 100644
--- a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIVariable.java
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/NIVariable.java
@@ -20,28 +20,28 @@ package org.netbeans.modules.nativeimage.api.debug;
 
 /**
  * Representation of a native variable.
- * @since 1.0
+ * @since 0.1
  */
 public interface NIVariable {
 
     /**
      * Name of the variable.
      *
-     * @since 1.0
+     * @since 0.1
      */
     String getName();
 
     /**
      * Type of the variable value.
      *
-     * @since 1.0
+     * @since 0.1
      */
     String getType();
 
     /**
      * String representation of the variable value.
      *
-     * @since 1.0
+     * @since 0.1
      */
     String getValue();
 
@@ -50,14 +50,14 @@ public interface NIVariable {
      * variable.
      *
      * @return the parent variable, or <code>null</code>.
-     * @since 1.0
+     * @since 0.1
      */
     NIVariable getParent();
 
     /**
      * Number of child variables (properties, or array elements) of this variable.
      *
-     * @since 1.0
+     * @since 0.1
      */
     int getNumChildren();
 
@@ -66,14 +66,14 @@ public interface NIVariable {
      * <code>from</code> index and up to and excluding <code>to</code> index will
      * be returned. If <code>from</code> is less than zero, all children are returned.
      *
-     * @since 1.0
+     * @since 0.1
      */
     NIVariable[] getChildren(int from, int to);
 
     /**
      * Get all variable's children.
      *
-     * @since 1.0
+     * @since 0.1
      */
     default NIVariable[] getChildren() {
         return getChildren(0, Integer.MAX_VALUE);
@@ -82,7 +82,7 @@ public interface NIVariable {
     /**
      * Get the full expression that this variable object represents.
      *
-     * @since 1.0
+     * @since 0.1
      */
     String getExpressionPath();
 
@@ -90,7 +90,7 @@ public interface NIVariable {
      * Get the frame this variable is associated with.
      *
      * @return the frame, or <code>null</code>.
-     * @since 1.0
+     * @since 0.1
      */
     NIFrame getFrame();
 }
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/StartDebugParameters.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/StartDebugParameters.java
new file mode 100644
index 0000000..2716714
--- /dev/null
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/api/debug/StartDebugParameters.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.netbeans.modules.nativeimage.api.debug;
+
+import java.io.File;
+import java.util.List;
+import org.netbeans.api.extexecution.ExecutionDescriptor;
+import org.openide.util.Lookup;
+
+/**
+ * Parameters passed to {@link NIDebugger#start(org.netbeans.modules.nativeimage.api.debug.StartDebugParameters, java.util.function.Consumer)}.
+ *
+ * @since 0.5
+ */
+public final class StartDebugParameters {
+
+    private final List<String> command;
+    private final File workingDirectory;
+    private final String debugger;
+    private final String displayName;
+    private final ExecutionDescriptor executionDescriptor;
+    private final Lookup contextLookup;
+
+    private StartDebugParameters(List<String> command, File workingDirectory, String debugger, String displayName, ExecutionDescriptor executionDescriptor, Lookup contextLookup) {
+        this.command = command;
+        this.workingDirectory = workingDirectory;
+        this.debugger = debugger;
+        this.displayName = displayName;
+        this.executionDescriptor = executionDescriptor;
+        this.contextLookup = contextLookup;
+    }
+
+    /**
+     * The command to run the native image.
+     *
+     * @return the command with arguments.
+     */
+    public List<String> getCommand() {
+        return command;
+    }
+
+    /**
+     * Working directory of the process.
+     *
+     * @return the working directory
+     */
+    public File getWorkingDirectory() {
+        return workingDirectory;
+    }
+
+    /**
+     * The native debugger command.
+     *
+     * @return the debugger command
+     */
+    public String getDebugger() {
+        return debugger;
+    }
+
+    /**
+     * Display name of the debugger task.
+     *
+     * @return the display name of debugger task
+     */
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    /**
+     * Execution descriptor that describes the runtime attributes.
+     *
+     * @return the execution descriptor
+     */
+    public ExecutionDescriptor getExecutionDescriptor() {
+        return executionDescriptor;
+    }
+
+    /**
+     * Context lookup. The lookup may contain other parameters, like ExplicitProcessParameters.
+     *
+     * @return the context lookup
+     */
+    public Lookup getContextLookup() {
+        return contextLookup;
+    }
+
+    /**
+     * Create a new debug parameters builder.
+     *
+     * @param command the command to run the native image.
+     * @return a new builder
+     */
+    public static Builder newBuilder(List<String> command) {
+        return new Builder(command);
+    }
+
+    /**
+     * Builder of start debug parameters.
+     *
+     * @since 0.5
+     */
+    public static final class Builder {
+
+        private final List<String> command;
+        private File workingDirectory;
+        private String debugger;
+        private String displayName;
+        private ExecutionDescriptor executionDescriptor;
+        private Lookup contextLookup;
+
+        Builder(List<String> command) {
+            this.command = command;
+        }
+
+        /**
+         * Set the working directory.
+         * @return the builder
+         */
+        public Builder workingDirectory(File workingDirectory) {
+            this.workingDirectory = workingDirectory;
+            return this;
+        }
+
+        /**
+         * Set the native debugger command.
+         * @return the builder
+         */
+        public Builder debugger(String debugger) {
+            this.debugger = debugger;
+            return this;
+        }
+
+        /**
+         * Set display name of the debugger task.
+         *
+         * @return the builder
+         */
+        public Builder displayName(String displayName) {
+            this.displayName = displayName;
+            return this;
+        }
+
+        /**
+         * Set execution descriptor that describes the runtime attributes.
+         *
+         * @return the builder
+         */
+        public Builder executionDescriptor(ExecutionDescriptor executionDescriptor) {
+            this.executionDescriptor = executionDescriptor;
+            return this;
+        }
+
+        /**
+         * Context lookup. The lookup may contain other parameters, like ExplicitProcessParameters.
+         *
+         * @return the builder
+         */
+        public Builder lookup(Lookup contextLookup) {
+            this.contextLookup = contextLookup;
+            return this;
+        }
+
+        /**
+         * Build the {@link StartDebugParameters} based on the properties set.
+         *
+         * @return a new instance of {@link StartDebugParameters}.
+         */
+        public StartDebugParameters build() {
+            return new StartDebugParameters(command, workingDirectory, debugger, displayName, executionDescriptor, contextLookup);
+        }
+    }
+}
diff --git a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/spi/debug/NIDebuggerProvider.java b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/spi/debug/NIDebuggerProvider.java
index 10a5b80..893f839 100644
--- a/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/spi/debug/NIDebuggerProvider.java
+++ b/ide/nativeimage.api/src/org/netbeans/modules/nativeimage/spi/debug/NIDebuggerProvider.java
@@ -33,6 +33,7 @@ import org.netbeans.modules.nativeimage.api.Symbol;
 import org.netbeans.modules.nativeimage.api.debug.NIFrame;
 import org.netbeans.modules.nativeimage.api.debug.NILineBreakpointDescriptor;
 import org.netbeans.modules.nativeimage.api.debug.NIVariable;
+import org.netbeans.modules.nativeimage.api.debug.StartDebugParameters;
 import org.netbeans.modules.nativeimage.spi.debug.filters.FrameDisplayer;
 import org.netbeans.modules.nativeimage.spi.debug.filters.VariableDisplayer;
 
@@ -40,7 +41,7 @@ import org.netbeans.modules.nativeimage.spi.debug.filters.VariableDisplayer;
  * Provider of the native image debugger.
  *
  * @author martin
- * @since 1.0
+ * @since 0.1
  */
 public interface NIDebuggerProvider {
 
@@ -52,7 +53,7 @@ public interface NIDebuggerProvider {
      * @param id a unique ID of the breakpoint
      * @param breakpointDescriptor the breakpoint descriptor
      * @return an instance of the native breakpoint
-     * @since 1.0
+     * @since 0.1
      */
     Breakpoint addLineBreakpoint(Object id, NILineBreakpointDescriptor breakpointDescriptor);
 
@@ -60,7 +61,7 @@ public interface NIDebuggerProvider {
      * Remove breakpoint with the given id.
      *
      * @param id the ID of the breakpoint to remove
-     * @since 1.0
+     * @since 0.1
      */
     void removeBreakpoint(Object id);
 
@@ -68,7 +69,7 @@ public interface NIDebuggerProvider {
      * Set a displayer of native frames.
      *
      * @param frameDisplayer translator of the native frame to it's displayed information
-     * @since 1.0
+     * @since 0.1
      */
     void setFrameDisplayer(FrameDisplayer frameDisplayer);
 
@@ -76,7 +77,7 @@ public interface NIDebuggerProvider {
      * Set a displayer of native variables.
      *
      * @param variablesDisplayer translator of native variables to displayed variables.
-     * @since 1.0
+     * @since 0.1
      */
     void setVariablesDisplayer(VariableDisplayer variablesDisplayer);
 
@@ -89,10 +90,33 @@ public interface NIDebuggerProvider {
      * @param displayName display name of the debugger task
      * @param executionDescriptor execution descriptor that describes the runtime attributes
      * @param startedEngine the corresponding DebuggerEngine is passed to this consumer
-     * @param finishedCallback notification of the execution finish
-     * @since 1.0
+     * @since 0.1
+     * @deprecated Use {@link #start(org.netbeans.modules.nativeimage.api.debug.StartDebugParameters, java.util.function.Consumer)}
      */
-    CompletableFuture<Void> start(List<String> command, File workingDirectory, String debugger, String displayName, ExecutionDescriptor executionDescriptor, Consumer<DebuggerEngine> startedEngine);
+    @Deprecated
+    default CompletableFuture<Void> start(List<String> command, File workingDirectory, String debugger, String displayName, ExecutionDescriptor executionDescriptor, Consumer<DebuggerEngine> startedEngine) {
+        StartDebugParameters parameters = StartDebugParameters.newBuilder(command)
+                .workingDirectory(workingDirectory)
+                .debugger(debugger)
+                .displayName(displayName)
+                .executionDescriptor(executionDescriptor)
+                .build();
+        return start(parameters, startedEngine);
+    }
+
+    /**
+     * Start the actual debugging session. Called typically after breakpoints are added.
+     * 
+     * @param debugParameters parameters to start the debugging with
+     * @param startedEngine the corresponding DebuggerEngine is passed to this consumer
+     * @return a future which is completed when the started debugger session finishes
+     * @since 0.5
+     */
+    default CompletableFuture<Void> start(StartDebugParameters debugParameters, Consumer<DebuggerEngine> startedEngine) {
+        CompletableFuture cf = new CompletableFuture();
+        cf.completeExceptionally(new UnsupportedOperationException());
+        return cf;
+    }
 
     /**
      * Attach to a process and create a debugging session. Called typically after breakpoints are added.
@@ -118,7 +142,7 @@ public interface NIDebuggerProvider {
      *                   when <code>null</code> the expression is used as the name
      * @param frame the frame to evaluate at
      * @return the completable future with the evaluation result
-     * @since 1.0
+     * @since 0.1
      */
     CompletableFuture<NIVariable> evaluateAsync(String expression, String resultName, NIFrame frame);
 
@@ -130,14 +154,14 @@ public interface NIDebuggerProvider {
      * @param length number of bytes to read
      * @return hexadecimal representation of the memory content, or <code>null</code>
      *         when the read is not successful
-     * @since 1.0
+     * @since 0.1
      */
     String readMemory(String address, long offset, int length);
 
     /**
      * Get version of the underlying native debugger.
      *
-     * @since 1.0
+     * @since 0.1
      */
     String getVersion();
 
@@ -153,10 +177,20 @@ public interface NIDebuggerProvider {
         return null;
     }
 
+    /**
+     * Provide a list of functions in the debuggee.
+     *
+     * @since 0.2
+     */
     default Map<SourceInfo, List<Symbol>> listFunctions(String name, boolean includeNondebug, int maxResults) {
         return null;
     }
 
+    /**
+     * Provide a list of global variables in the debuggee.
+     *
+     * @since 0.2
+     */
     default Map<SourceInfo, List<Symbol>> listVariables(String name, boolean includeNondebug, int maxResults) {
         return null;
     }
diff --git a/java/gradle.java/arch.xml b/java/gradle.java/arch.xml
index f9d2db2..7c06a5f 100644
--- a/java/gradle.java/arch.xml
+++ b/java/gradle.java/arch.xml
@@ -110,6 +110,18 @@
          in action's context Lookup. See <a href="@TOP@/org/netbeans/modules/gradle/java/api/ProjectActions.html#TOKEN_JAVAEXEC_ARGS">ProjectActions.TOKEN_JAVAEXEC_ARGS</a>
          for more details.
      </api>
+     <api category="stable" group="property" name="javaExec.runWorkingDir" type="export">
+         {@code ${javaExec.runWorkingDir}} token is replaced in action mappings with working directory retrieved
+         from <a href="@org-netbeans-modules-extexecution-base@/org/netbeans/api/extexecution/base/ExplicitProcessParameters.html">ExplicitProcessParameters</a>
+         in action's context Lookup. See <a href="@TOP@/org/netbeans/modules/gradle/java/api/ProjectActions.html#TOKEN_JAVAEXEC_CWD">ProjectActions.TOKEN_JAVAEXEC_CWD</a>
+         for more details.
+     </api>
+     <api category="stable" group="property" name="javaExec.runEnvironment" type="export">
+         {@code ${javaExec.runEnvironment}} token is replaced in action mappings with environment variables retrieved
+         from <a href="@org-netbeans-modules-extexecution-base@/org/netbeans/api/extexecution/base/ExplicitProcessParameters.html">ExplicitProcessParameters</a>
+         in action's context Lookup. See <a href="@TOP@/org/netbeans/modules/gradle/java/api/ProjectActions.html#TOKEN_JAVAEXEC_ENV">ProjectActions.TOKEN_JAVAEXEC_ENV</a>
+         for more details.
+     </api>
  </answer>
 
  <answer id="resources-file">
diff --git a/java/gradle.java/nbproject/project.xml b/java/gradle.java/nbproject/project.xml
index b8ba6b9..30877a7 100644
--- a/java/gradle.java/nbproject/project.xml
+++ b/java/gradle.java/nbproject/project.xml
@@ -31,7 +31,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>2.14</specification-version>
+                        <specification-version>2.21</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
@@ -110,7 +110,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.18</specification-version>
+                        <specification-version>1.20</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml b/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml
index 77de6e4..6a3b210 100644
--- a/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml
+++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml
@@ -50,21 +50,21 @@
         </action>
 
         <action name="run.single">
-            <args>-PrunClassName=${selectedClass} runSingle  ${javaExec.jvmArgs} ${javaExec.args}</args>
+            <args>-PrunClassName=${selectedClass} ${javaExec.workingDir} ${javaExec.environment} runSingle --stacktrace ${javaExec.jvmArgs} ${javaExec.args}</args>
         </action>
 
         <action name="debug.single">
-            <args>-PrunClassName=${selectedClass} runSingle --debug-jvm  ${javaExec.jvmArgs} ${javaExec.args}</args>
+            <args>-PrunClassName=${selectedClass} ${javaExec.workingDir} ${javaExec.environment} runSingle --stacktrace --debug-jvm  ${javaExec.jvmArgs} ${javaExec.args}</args>
         </action>
     </apply-for>
 
     <apply-for plugins="application">
         <action name="run">
-            <args>run ${javaExec.jvmArgs} ${javaExec.args}</args>
+            <args>${javaExec.workingDir} ${javaExec.environment} run ${javaExec.jvmArgs} ${javaExec.args}</args>
         </action>
 
         <action name="debug">
-            <args>run --debug-jvm  ${javaExec.jvmArgs} ${javaExec.args}</args>
+            <args>${javaExec.workingDir} ${javaExec.environment} run --debug-jvm  ${javaExec.jvmArgs} ${javaExec.args}</args>
         </action>
     </apply-for>
 
@@ -88,7 +88,7 @@
             <actions>
                 <apply-for plugins="java">
                     <action name="run.single">
-                        <args>-PrunClassName=${selectedClass} runSingle --continuous ${javaExec.jvmArgs} ${javaExec.args}</args>
+                        <args>-PrunClassName=${selectedClass} ${javaExec.workingDir} ${javaExec.environment} runSingle --continuous ${javaExec.jvmArgs} ${javaExec.args}</args>
                     </action>
                     <action name="test.single">
                         <args>cleanTest test --tests "${selectedClass}" --continuous</args>
@@ -111,7 +111,7 @@
                 </apply-for>
                 <apply-for plugins="application">
                     <action name="run">
-                        <args>run --continuous ${javaExec.jvmArgs} ${javaExec.args}</args>
+                        <args>${javaExec.workingDir} ${javaExec.environment} run --continuous ${javaExec.jvmArgs} ${javaExec.args}</args>
                     </action>
                     <action name="debug">
                         <args></args>
diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/api/ProjectActions.java b/java/gradle.java/src/org/netbeans/modules/gradle/java/api/ProjectActions.java
index a382e74..b4cafee 100644
--- a/java/gradle.java/src/org/netbeans/modules/gradle/java/api/ProjectActions.java
+++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/api/ProjectActions.java
@@ -41,15 +41,34 @@ public final class ProjectActions {
      * {@codesnippet JavaExecTokenProviderTest#testExamplePassJvmAndArguments}
      * </div>
      */
-    public static String TOKEN_JAVAEXEC_JVMARGS = "javaExec.jvmArgs";
+    public static String TOKEN_JAVAEXEC_JVMARGS = "javaExec.jvmArgs"; // NOI18N
 
     /**
      * Replaceable token for program parameters as a commandline option. Generates --args <i>&lt;parameter-list></i>, if the extra parameters are present, otherwise
      * generates an empty String. See {@link #TOKEN_JAVAEXEC_JVMARGS} for code examples.
      * @since 1.9
      */
-    public static String TOKEN_JAVAEXEC_ARGS = "javaExec.args";
+    public static String TOKEN_JAVAEXEC_ARGS = "javaExec.args"; // NOI18N
 
+    /**
+     * Replaceable token for program working directory. Generates project property for NB Tooling Gradle plugin, which is used in <code>action-mapping.xml</code>
+     * and can be customized by the user. This feature cooperates with NetBeans Tooling Gradle plugin provided by org.netbeans.gradle module.
+     * The Gradle Java project support consumes {@link ExplicitProcessParameters} from the action's context Lookup, and populates the replaceable token mapping
+     * from {@link ExplicitProcessParameters#getWorkingDirectory()}.
+     *
+     * @since 1.15
+     */
+    public static String TOKEN_JAVAEXEC_CWD = "javaExec.workingDir"; // NOI18N
+
+    /**
+     * Replaceable token for program environment variables. Generates project property for NB Tooling Gradle plugin, which is used in <code>action-mapping.xml</code>
+     * and can be customized by the user. This feature cooperates with NetBeans Tooling Gradle plugin provided by org.netbeans.gradle module.
+     * The Gradle Java project support consumes {@link ExplicitProcessParameters} from the action's context Lookup, and populates the replaceable token mapping
+     * from {@link ExplicitProcessParameters#getEnvironmentVariables()}.
+     *
+     * @since 1.15
+     */
+    public static String TOKEN_JAVAEXEC_ENV = "javaExec.environment"; // NOI18N
 
     private ProjectActions() {}
 }
diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java b/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java
index f477056..f0096e1 100644
--- a/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java
+++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java
@@ -64,6 +64,20 @@ public class JavaExecTokenProvider implements ReplaceTokenProvider {
     public static String TOKEN_JAVAEXEC_ARGS = ProjectActions.TOKEN_JAVAEXEC_ARGS;
 
     /**
+     * Replaceable token for working directory project property. Generates project property for NB Tooling Gradle plugin, if the working directory is present, otherwise
+     * generates an empty String.
+     * @see #TOKEN_JAVA_CWD
+     */
+    public static String TOKEN_JAVAEXEC_CWD = ProjectActions.TOKEN_JAVAEXEC_CWD;
+
+    /**
+     * Replaceable token for environment variables project property. Generates project property for NB Tooling Gradle plugin, if the environment variables are present,
+     * otherwise generates an empty String.
+     * @see #TOKEN_JAVA_ENV
+     */
+    public static String TOKEN_JAVAEXEC_ENV = ProjectActions.TOKEN_JAVAEXEC_ENV;
+
+    /**
      * Replaceable token for JVM arguments. Generates escaped / quoted arguments as a single String.
      */
     public static String TOKEN_JAVA_ARGS = "java.args"; // NOI18N
@@ -73,7 +87,19 @@ public class JavaExecTokenProvider implements ReplaceTokenProvider {
      * a space-delimited String.
      */
     public static String TOKEN_JAVA_JVMARGS = "java.jvmArgs"; // NOI18N
-    
+
+    /**
+     * Replaceable token for application working directory.
+     */
+    public static String TOKEN_JAVA_CWD = "java.workingDir"; // NOI18N
+
+    /**
+     * Replaceable token for environment variables. Generates escaped / quoted variable assignments as a single String.
+     * Expression <code>VAR_NAME=VAR_VALUE</code> sets environment variable of name <code>VAR_NAME</code> to value
+     * <code>VAR_VALUE</code>, expression <code>!VAR_NAME</code> unsets environment variable <code>VAR_NAME</code>.
+     */
+    public static String TOKEN_JAVA_ENV = "java.environment"; // NOI18N
+
     private static final Set<String> TOKENS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
             TOKEN_JAVAEXEC_ARGS, TOKEN_JAVAEXEC_JVMARGS,
             TOKEN_JAVA_ARGS, TOKEN_JAVA_JVMARGS
@@ -144,8 +170,12 @@ public class JavaExecTokenProvider implements ReplaceTokenProvider {
         Map<String, String> result = new HashMap<>();
         result.put(TOKEN_JAVAEXEC_ARGS, ""); // NOI18N
         result.put(TOKEN_JAVAEXEC_JVMARGS, ""); // NOI18N
+        result.put(TOKEN_JAVAEXEC_CWD, ""); // NOI18N
+        result.put(TOKEN_JAVAEXEC_ENV, ""); // NOI18N
         result.put(TOKEN_JAVA_ARGS, ""); // NOI18N
         result.put(TOKEN_JAVA_JVMARGS, ""); // NOI18N
+        result.put(TOKEN_JAVA_CWD, ""); // NOI18N
+        result.put(TOKEN_JAVA_ENV, ""); // NOI18N
         if (extraArgs.isEmpty() && contextParams.isEmpty()) {
             return result;
         }
@@ -178,6 +208,32 @@ public class JavaExecTokenProvider implements ReplaceTokenProvider {
             result.put(TOKEN_JAVA_ARGS, args);
             result.put(TOKEN_JAVAEXEC_ARGS, "--args " + prop);  // NOI18N
         }
+        if (changedParams.getWorkingDirectory() != null) {
+            String wd = Utilities.escapeParameters(new String[] {
+                changedParams.getWorkingDirectory().getAbsolutePath()
+            });
+            result.put(TOKEN_JAVA_CWD, wd);
+            String prop = Utilities.escapeParameters(new String[] {
+                "-PrunWorkingDir=" + wd     // NOI18N
+            });
+            result.put(TOKEN_JAVAEXEC_CWD, prop);
+        }
+        if (!changedParams.getEnvironmentVariables().isEmpty()) {
+            List<String> envParams = new ArrayList<>(changedParams.getEnvironmentVariables().size());
+            for (Map.Entry<String, String> entry : changedParams.getEnvironmentVariables().entrySet()) {
+                if (entry.getValue() != null) {
+                    envParams.add(entry.getKey() + "=" + entry.getValue());
+                } else {
+                    envParams.add("!" + entry.getKey());
+                }
+            }
+            String env = Utilities.escapeParameters(envParams.toArray(new String[envParams.size()]));
+            result.put(TOKEN_JAVA_ENV, env);
+            String prop = Utilities.escapeParameters(new String[] {
+                "-PrunEnvironment=" + env   // NOI18N
+            });
+            result.put(TOKEN_JAVAEXEC_ENV, prop);
+        }
         return result;
     }
 }
diff --git a/java/gradle.java/test/unit/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProviderTest.java b/java/gradle.java/test/unit/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProviderTest.java
index 50943f0..197924c 100644
--- a/java/gradle.java/test/unit/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProviderTest.java
+++ b/java/gradle.java/test/unit/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProviderTest.java
@@ -18,10 +18,12 @@
  */
 package org.netbeans.modules.gradle.java.execute;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -174,6 +176,38 @@ public class JavaExecTokenProviderTest extends NbTestCase {
         assertParamList(map.get("java.args"), THREE_PROPS);
     }
     
+    public void testAppCWD() throws Exception {
+        JavaExecTokenProvider jetp = new JavaExecTokenProvider(createSimpleJavaProject());
+        
+        File wdf = new File("A test working/directory");
+        String wd = wdf.getAbsolutePath();
+        ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                workingDirectory(wdf).
+                build();
+
+        Map<String, String> map = jetp.createReplacements(ActionProvider.COMMAND_RUN, Lookups.singleton(params));
+        assertEquals(6, map.size());
+
+        assertParamList(map.get("java.workingDir"), wd);
+        assertParamList(map.get("javaExec.workingDir"), "-PrunWorkingDir=\"" + wd + "\"");
+    }
+    
+    public void testAppEnvironment() throws Exception {
+        JavaExecTokenProvider jetp = new JavaExecTokenProvider(createSimpleJavaProject());
+        
+        Map<String, String> envVars = new LinkedHashMap<>();
+        envVars.put("TestVar1", "Env Value 1");
+        envVars.put("TestVar2", null);
+        ExplicitProcessParameters params = ExplicitProcessParameters.builder().
+                environmentVariables(envVars).
+                build();
+
+        Map<String, String> map = jetp.createReplacements(ActionProvider.COMMAND_RUN, Lookups.singleton(params));
+        assertEquals(6, map.size());
+
+        assertParamList(map.get("java.environment"), new String[]{"TestVar1=Env Value 1",  "!TestVar2"});
+        assertParamList(map.get("javaExec.environment"), "-PrunEnvironment=\"TestVar1=Env Value 1\" !TestVar2");
+    }
     
     /**
      * Note: this code is not actually run, as it would require an entire
diff --git a/java/java.lsp.server/nbproject/project.xml b/java/java.lsp.server/nbproject/project.xml
index ce9ea03..6e882ce 100644
--- a/java/java.lsp.server/nbproject/project.xml
+++ b/java/java.lsp.server/nbproject/project.xml
@@ -206,7 +206,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.17</specification-version>
+                        <specification-version>1.20</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
@@ -353,7 +353,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>0</release-version>
-                        <specification-version>0.4</specification-version>
+                        <specification-version>0.5</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
index ccfcece..a30c6c2 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
@@ -29,6 +29,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
@@ -72,6 +73,7 @@ import org.netbeans.modules.java.lsp.server.progress.ProgressOperationListener;
 import org.netbeans.modules.java.lsp.server.progress.TestProgressHandler;
 import org.netbeans.modules.java.nativeimage.debugger.api.NIDebugRunner;
 import org.netbeans.modules.nativeimage.api.debug.NIDebugger;
+import org.netbeans.modules.nativeimage.api.debug.StartDebugParameters;
 import org.netbeans.spi.project.ActionProgress;
 import org.netbeans.spi.project.ActionProvider;
 import org.netbeans.spi.project.ProjectConfiguration;
@@ -173,16 +175,7 @@ public abstract class NbLaunchDelegate {
             W writer = new W();
             CompletableFuture<Pair<ActionProvider, String>> commandFuture = findTargetWithPossibleRebuild(toRun, singleMethod, debug, testRun, ioContext);
             commandFuture.thenAccept((providerAndCommand) -> {
-                List<String> args = argsToStringList(launchArguments.get("args"));
-                List<String> vmArgs = argsToStringList(launchArguments.get("vmArgs"));
-                ExplicitProcessParameters params = ExplicitProcessParameters.empty();
-                if (!(args.isEmpty() && vmArgs.isEmpty())) {
-                    ExplicitProcessParameters.Builder bld = ExplicitProcessParameters.builder();
-                    bld.launcherArgs(vmArgs);
-                    bld.args(args);
-                    bld.replaceArgs(false);
-                    params = bld.build();
-                }
+                ExplicitProcessParameters params = createExplicitProcessParameters(launchArguments);
                 OperationContext ctx = OperationContext.find(Lookup.getDefault());
                 ctx.addProgressOperationListener(null, new ProgressOperationListener() {
                     @Override
@@ -320,6 +313,7 @@ public abstract class NbLaunchDelegate {
                     .showSuspended(true)
                     .frontWindowOnError(true)
                     .controllable(true);
+            ExplicitProcessParameters params = createExplicitProcessParameters(launchArguments);
             Lookup launchCtx = new ProxyLookup(
                     Lookups.fixed(ioContext, progress),
                     Lookup.getDefault()
@@ -335,7 +329,7 @@ public abstract class NbLaunchDelegate {
                     });
                     Lookups.executeWith(execLookup, () -> {
                         String miDebugger = (String) launchArguments.get("miDebugger");
-                        startNativeDebug(nativeImageFile, args, miDebugger, context, ed, launchFuture, debugProgress);
+                        startNativeDebug(nativeImageFile, args, miDebugger, context, ed, Lookups.fixed(params), launchFuture, debugProgress);
                     });
                 });
             } else {
@@ -344,19 +338,64 @@ public abstract class NbLaunchDelegate {
                     notifyFinished(context, exitCode != null && exitCode == 0);
                 });
                 Lookups.executeWith(execLookup, () -> {
-                    execNative(nativeImageFile, args, context, ed, launchFuture);
+                    execNative(nativeImageFile, args, context, ed, params, launchFuture);
                 });
             }
         }
         return launchFuture;
     }
 
-    private static void execNative(File nativeImageFile, List<String> args, DebugAdapterContext context, ExecutionDescriptor executionDescriptor, CompletableFuture<Void> launchFuture) {
+    private static ExplicitProcessParameters createExplicitProcessParameters(Map<String, Object> launchArguments) {
+        List<String> args = argsToStringList(launchArguments.get("args"));
+        List<String> vmArgs = argsToStringList(launchArguments.get("vmArgs"));
+        String cwd = Objects.toString(launchArguments.get("cwd"), null);
+        Object envObj = launchArguments.get("env");
+        Map<String, String> env = envObj != null ? (Map<String, String>) envObj : Collections.emptyMap();
+        ExplicitProcessParameters.Builder bld = ExplicitProcessParameters.builder();
+        if (!args.isEmpty()) {
+            bld.launcherArgs(vmArgs);
+        }
+        if (!vmArgs.isEmpty()) {
+            bld.args(args);
+        }
+        bld.replaceArgs(false);
+        if (cwd != null) {
+            bld.workingDirectory(new File(cwd));
+        }
+        if (!env.isEmpty()) {
+            bld.environmentVariables(env);
+        }
+        ExplicitProcessParameters params = bld.build();
+        return params;
+    }
+
+    private static void execNative(File nativeImageFile, List<String> args,
+                                   DebugAdapterContext context,
+                                   ExecutionDescriptor executionDescriptor,
+                                   ExplicitProcessParameters params,
+                                   CompletableFuture<Void> launchFuture) {
         ExecutionService.newService(() -> {
             launchFuture.complete(null);
-            List<String> command = args.isEmpty() ? Collections.singletonList(nativeImageFile.getAbsolutePath()) : join(nativeImageFile.getAbsolutePath(), args);
+            List<String> command = join(nativeImageFile.getAbsolutePath(), args);
             try {
-                return new ProcessBuilder(command).start();
+                ProcessBuilder pb = new ProcessBuilder(command);
+                File workingDirectory = params.getWorkingDirectory();
+                if (workingDirectory != null) {
+                    pb.directory(workingDirectory);
+                }
+                if (!params.getEnvironmentVariables().isEmpty()) {
+                    Map<String, String> environment = pb.environment();
+                    for (Map.Entry<String, String> entry : params.getEnvironmentVariables().entrySet()) {
+                        String env = entry.getKey();
+                        String val = entry.getValue();
+                        if (val != null) {
+                            environment.put(env, val);
+                        } else {
+                            environment.remove(env);
+                        }
+                    }
+                }
+                return pb.start();
             } catch (IOException ex) {
                 ErrorUtilities.completeExceptionally(launchFuture,
                     "Failed to run debuggee native image: " + ex.getLocalizedMessage(),
@@ -367,18 +406,32 @@ public abstract class NbLaunchDelegate {
     }
 
     private static List<String> join(String first, List<String> next) {
+        if (next.isEmpty()) {
+            return Collections.singletonList(first);
+        }
         List<String> joined = new ArrayList<>(next.size() + 1);
         joined.add(first);
         joined.addAll(next);
         return joined;
     }
 
-    private static void startNativeDebug(File nativeImageFile, List<String> args, String miDebugger, DebugAdapterContext context, ExecutionDescriptor executionDescriptor, CompletableFuture<Void> launchFuture, ActionProgress debugProgress) {
+    private static void startNativeDebug(File nativeImageFile, List<String> args,
+                                         String miDebugger, DebugAdapterContext context,
+                                         ExecutionDescriptor executionDescriptor,
+                                         Lookup contextLookup,
+                                         CompletableFuture<Void> launchFuture,
+                                         ActionProgress debugProgress) {
         AtomicReference<NbDebugSession> debugSessionRef = new AtomicReference<>();
         CompletableFuture<Void> finished = new CompletableFuture<>();
+        List<String> command = join(nativeImageFile.getAbsolutePath(), args);
+        StartDebugParameters.Builder parametersBuilder = StartDebugParameters.newBuilder(command)
+                .debugger(miDebugger)
+                .executionDescriptor(executionDescriptor)
+                .lookup(contextLookup);
+        StartDebugParameters parameters = parametersBuilder.build();
         NIDebugger niDebugger;
         try {
-            niDebugger = NIDebugRunner.start(nativeImageFile, args, miDebugger, null, null, executionDescriptor, engine -> {
+            niDebugger = NIDebugRunner.start(nativeImageFile, parameters, null, engine -> {
                 Session session = engine.lookupFirst(null, Session.class);
                 NbDebugSession debugSession = new NbDebugSession(session);
                 debugSessionRef.set(debugSession);
@@ -405,7 +458,7 @@ public abstract class NbLaunchDelegate {
     }
 
     @NonNull
-    private List<String> argsToStringList(Object o) {
+    private static List<String> argsToStringList(Object o) {
         if (o == null) {
             return Collections.emptyList();
         }
diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json
index 92bb6f0..c25d787 100644
--- a/java/java.lsp.server/vscode/package.json
+++ b/java/java.lsp.server/vscode/package.json
@@ -153,6 +153,21 @@
 								"description": "Arguments for the Java VM",
 								"default": null
 							},
+							"cwd": {
+								"type": [
+									"string",
+									"null"
+								],
+								"description": "Working directory for the program execution",
+								"default": null
+							},
+							"env": {
+								"type": [
+									"object"
+								],
+								"description": "Environment variables for the program execution",
+								"default": {}
+							},
 							"launchConfiguration": {
 								"type": [
 									"string",
diff --git a/java/java.nativeimage.debugger/nbproject/project.xml b/java/java.nativeimage.debugger/nbproject/project.xml
index b54add0..5e7fb4b 100644
--- a/java/java.nativeimage.debugger/nbproject/project.xml
+++ b/java/java.nativeimage.debugger/nbproject/project.xml
@@ -85,7 +85,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>0</release-version>
-                        <specification-version>0.4</specification-version>
+                        <specification-version>0.5</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/actions/NIAttachCustomizer.java b/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/actions/NIAttachCustomizer.java
index 6cbd10c..0b5eb01 100644
--- a/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/actions/NIAttachCustomizer.java
+++ b/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/actions/NIAttachCustomizer.java
@@ -39,6 +39,7 @@ import org.netbeans.api.project.FileOwnerQuery;
 import org.netbeans.api.project.Project;
 import org.netbeans.modules.java.nativeimage.debugger.actions.Processes.ProcessInfo;
 import org.netbeans.modules.java.nativeimage.debugger.api.NIDebugRunner;
+import org.netbeans.modules.nativeimage.api.debug.StartDebugParameters;
 import org.netbeans.spi.debugger.ui.Controller;
 import static org.netbeans.spi.debugger.ui.Controller.PROP_VALID;
 import org.netbeans.spi.debugger.ui.PersistentController;
diff --git a/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/api/NIDebugRunner.java b/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/api/NIDebugRunner.java
index 0db96e4..a1eee90 100644
--- a/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/api/NIDebugRunner.java
+++ b/java/java.nativeimage.debugger/src/org/netbeans/modules/java/nativeimage/debugger/api/NIDebugRunner.java
@@ -32,6 +32,7 @@ import org.netbeans.modules.nativeimage.api.debug.NIDebugger;
 import org.netbeans.modules.java.nativeimage.debugger.breakpoints.JPDABreakpointsHandler;
 import org.netbeans.modules.java.nativeimage.debugger.displayer.JavaFrameDisplayer;
 import org.netbeans.modules.java.nativeimage.debugger.displayer.JavaVariablesDisplayer;
+import org.netbeans.modules.nativeimage.api.debug.StartDebugParameters;
 import static org.netbeans.spi.project.ActionProvider.COMMAND_DEBUG;
 
 import org.openide.DialogDisplayer;
@@ -61,7 +62,9 @@ public final class NIDebugRunner {
      * @param startedEngine consumer of the started {@link DebuggerEngine}.
      * @return an instance of {@link NIDebugger}.
      * @throws IllegalStateException when the native debugger is not available.
+     * @deprecated Use {@link #start(File, StartDebugParameters, Project, Consumer)} instead.
      */
+    @Deprecated
     public static NIDebugger start(File niFile, List<String> arguments, String debuggerCommand, Project project, String displayName, ExecutionDescriptor executionDescriptor, Consumer<DebuggerEngine> startedEngine) throws IllegalStateException {
         JavaVariablesDisplayer variablesDisplayer = new JavaVariablesDisplayer();
         JavaFrameDisplayer frameDisplayer = new JavaFrameDisplayer(project);
@@ -91,6 +94,28 @@ public final class NIDebugRunner {
         return debugger;
     }
 
+    public static NIDebugger start(File niFile, StartDebugParameters debugParameters, Project project, Consumer<DebuggerEngine> startedEngine) throws IllegalStateException {
+        JavaVariablesDisplayer variablesDisplayer = new JavaVariablesDisplayer();
+        JavaFrameDisplayer frameDisplayer = new JavaFrameDisplayer(project);
+        NIDebugger debugger = NIDebugger.newBuilder()
+                .frameDisplayer(frameDisplayer)
+                .variablesDisplayer(variablesDisplayer)
+                .build();
+        variablesDisplayer.setDebugger(debugger);
+        JPDABreakpointsHandler breakpointsHandler = new JPDABreakpointsHandler(niFile, debugger);
+        DialogDisplayer displayer = DialogDisplayer.getDefault(); // The launcher might provide a special displayer in the lookup. This is why we grab it eagerly.
+        debugger.start(debugParameters,
+                (engine) -> {
+                    if (startedEngine != null) {
+                        startedEngine.accept(engine);
+                    }
+                }).thenRun(() -> {
+                    breakpointsHandler.dispose();
+                });
+        checkVersion(debugger.getVersion(), displayer);
+        return debugger;
+    }
+
     /**
      * Attach debugger to a Native Image.
      *
diff --git a/java/maven/nbproject/project.xml b/java/maven/nbproject/project.xml
index 9d105e2..f8fb496 100644
--- a/java/maven/nbproject/project.xml
+++ b/java/maven/nbproject/project.xml
@@ -140,7 +140,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.17</specification-version>
+                        <specification-version>1.20</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/maven/src/org/netbeans/modules/maven/debug/DebuggerChecker.java b/java/maven/src/org/netbeans/modules/maven/debug/DebuggerChecker.java
index 974f98e..8df8831 100644
--- a/java/maven/src/org/netbeans/modules/maven/debug/DebuggerChecker.java
+++ b/java/maven/src/org/netbeans/modules/maven/debug/DebuggerChecker.java
@@ -248,25 +248,35 @@ public class DebuggerChecker implements LateBoundPrerequisitesChecker, Execution
                 }
                 String val = start.execute(context.getInputOutput());
                 for (Map.Entry<String,String> entry : NbCollections.checkedMapByFilter(config.getProperties(), String.class, String.class, true).entrySet()) {
-                    StringBuilder buf = new StringBuilder(entry.getValue());
+                    String value = entry.getValue();
+                    StringBuilder buf = null;
                     String replaceItem = "${jpda.address}"; //NOI18N
-                    int index = buf.indexOf(replaceItem);
+                    int index = value.indexOf(replaceItem);
                     while (index > -1) {
                         String newItem = val;
                         newItem = newItem == null ? "" : newItem; //NOI18N
+                        if (buf == null) {
+                            buf = new StringBuilder(value);
+                        }
                         buf.replace(index, index + replaceItem.length(), newItem);
                         index = buf.indexOf(replaceItem);
                     }
                     // debug must properly update debug port in runconfig...
                     if(entry.getKey().equals(MAVENSUREFIREDEBUG)) {
                         String address = "address="; //NOI18N
-                        index = buf.indexOf(address);
+                        index = value.indexOf(address);
                         if(index > -1) {
+                            if (buf == null) {
+                                buf = new StringBuilder(value);
+                            }
                             buf.replace(index + 8, buf.length(), val);
                         }
                     }
+                    if (buf != null) { // Change the value when necessary only, to keep it's identity otherwise.
+                        value = buf.toString();
+                    }
                     //                System.out.println("setting property=" + key + "=" + buf.toString());
-                    config.setProperty(entry.getKey(), buf.toString());
+                    config.setProperty(entry.getKey(), value);
                 }
                 config.setProperty("jpda.address", val); //NOI18N
             } catch (Throwable th) {
diff --git a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java
index 45027bb..18591d8 100644
--- a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java
+++ b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java
@@ -42,6 +42,7 @@ import org.netbeans.modules.maven.NbMavenProjectImpl;
 import org.netbeans.modules.maven.api.NbMavenProject;
 import org.netbeans.modules.maven.classpath.MavenSourcesImpl;
 import org.netbeans.modules.maven.configurations.M2ConfigProvider;
+import org.netbeans.modules.maven.runjar.MavenExecuteUtils;
 import org.netbeans.modules.maven.spi.actions.ActionConvertor;
 import org.netbeans.modules.maven.spi.actions.ReplaceTokenProvider;
 import org.netbeans.spi.project.ActionProvider;
diff --git a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java
index d303f7f..471ac41 100644
--- a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java
+++ b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java
@@ -37,6 +37,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.logging.Level;
@@ -79,6 +80,7 @@ import org.netbeans.modules.maven.execute.cmd.ShellConstructor;
 import org.netbeans.modules.maven.indexer.api.RepositoryIndexer;
 import org.netbeans.modules.maven.indexer.api.RepositoryPreferences;
 import org.netbeans.modules.maven.options.MavenSettings;
+import org.netbeans.modules.maven.runjar.MavenExecuteUtils;
 import org.netbeans.spi.project.ui.support.BuildExecutionSupport;
 import org.openide.LifecycleManager;
 import org.openide.awt.HtmlBrowser;
@@ -123,9 +125,9 @@ import org.openide.windows.OutputListener;
  * @author  Svata Dedic (svatopluk.dedic@gmail.com)
  */
 public class MavenCommandLineExecutor extends AbstractMavenExecutor {
-    static final String ENV_PREFIX = "Env."; //NOI18N
+    static final String ENV_PREFIX = MavenExecuteUtils.ENV_PREFIX;
     static final String INTERNAL_PREFIX = "NbIde."; //NOI18N
-    static final String ENV_JAVAHOME = "Env.JAVA_HOME"; //NOI18N
+    static final String ENV_JAVAHOME = ENV_PREFIX + "JAVA_HOME"; //NOI18N
 
     private static final String KEY_UUID = "NB_EXEC_MAVEN_PROCESS_UUID"; //NOI18N
 
@@ -363,7 +365,7 @@ public class MavenCommandLineExecutor extends AbstractMavenExecutor {
         out.waitFor();
         return executionresult;
     }
-
+    
     private void kill(Process prcs, String uuid) {
         Map<String, String> env = new HashMap<String, String>();
         env.put(KEY_UUID, uuid);
@@ -674,7 +676,11 @@ public class MavenCommandLineExecutor extends AbstractMavenExecutor {
                 continue;// #191374: would prevent bin/mvn from using selected installation
             }
             // TODO: do we really put *all* the env vars there? maybe filter, M2_HOME and JDK_HOME?
-            builder.environment().put(env, val);
+            if (MavenExecuteUtils.isEnvRemovedValue(val)) {
+                builder.environment().remove(env);
+            } else {
+                builder.environment().put(env, val);
+            }
             if (!env.equals(CosChecker.NETBEANS_PROJECT_MAPPINGS)
                     && !env.equals(NETBEANS_MAVEN_COMMAND_LINE)) { //don't show to user
                 display.append(Utilities.escapeParameters(new String[] {env + "=" + val})).append(' '); // NOI18N
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java b/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java
index a5438e0..234115b 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java
@@ -18,6 +18,7 @@
  */
 package org.netbeans.modules.maven.runjar;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -122,6 +123,20 @@ public class LaunchArgPrereqsChecker implements LateBoundPrerequisitesChecker {
             config.setProperty(MavenExecuteUtils.RUN_APP_PARAMS, 
                     MavenExecuteUtils.joinParameters(appArgsValue));
         }
+        File workingDirectory = injectParams.getWorkingDirectory();
+        if (workingDirectory != null) {
+            config.setProperty(MavenExecuteUtils.RUN_WORKDIR,
+                    workingDirectory.getAbsolutePath());
+        }
+        Map<String, String> environmentVariables = injectParams.getEnvironmentVariables();
+        for (Map.Entry<String, String> env : environmentVariables.entrySet()) {
+            String value = env.getValue();
+            if (value == null) {
+                // The environment variable is to be removed when the value is null
+                value = MavenExecuteUtils.ENV_REMOVED;
+            }
+            config.setProperty(MavenExecuteUtils.ENV_PREFIX + env.getKey(), value);
+        }
         return true;
     }
 }
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java b/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java
index 4367fa4..6c747af 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java
@@ -84,6 +84,9 @@ public final class MavenExecuteUtils {
     static final String DEFAULT_DEBUG_PARAMS = "-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address}"; //NOI18N
     static final String DEFAULT_EXEC_ARGS_CLASSPATH2 =  "${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs}"; // NOI18N
 
+    public static final String ENV_PREFIX = "Env."; // NOI18N
+    public static final String ENV_REMOVED = new String("null"); // A special instance for env vars to be removed. // NOI18N
+
     /**
      * ID of the 'profile' action.
      */
@@ -513,6 +516,10 @@ public final class MavenExecuteUtils {
         return null;
     }
     
+    public static boolean isEnvRemovedValue(String value) {
+        return value == ENV_REMOVED; // It's crutial to test the instance identity
+    }
+    
     /**
      * Creates a helper to edit the mapping instances. Individual settings can be
      * inspected by getters and changed by setters on the helper, changes can be then
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java b/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java
index 811c713..3846bfd 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java
@@ -19,13 +19,13 @@
 
 package org.netbeans.modules.maven.runjar;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Stream;
-import org.apache.maven.wagon.Streams;
+
 import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
 import org.netbeans.api.extexecution.startup.StartupExtender;
 import org.netbeans.api.project.Project;
@@ -206,6 +206,20 @@ public class RunJarStartupArgs implements LateBoundPrerequisitesChecker {
             config.setProperty(MavenExecuteUtils.RUN_APP_PARAMS, 
                     MavenExecuteUtils.joinParameters(appArgsValue));
         }
+        File workingDirectory = injectParams.getWorkingDirectory();
+        if (workingDirectory != null) {
+            config.setProperty(MavenExecuteUtils.RUN_WORKDIR,
+                    workingDirectory.getAbsolutePath());
+        }
+        Map<String, String> environmentVariables = injectParams.getEnvironmentVariables();
+        for (Map.Entry<String, String> env : environmentVariables.entrySet()) {
+            String value = env.getValue();
+            if (value == null) {
+                // The environment variable is to be removed when the value is null
+                value = MavenExecuteUtils.ENV_REMOVED;
+            }
+            config.setProperty(MavenExecuteUtils.ENV_PREFIX + env.getKey(), value);
+        }
 
         if ("test".equals(props.get("exec.classpathScope"))) {
             isTestScope = true;
diff --git a/java/maven/test/unit/src/org/netbeans/modules/maven/execute/MavenExecutionTestBase.java b/java/maven/test/unit/src/org/netbeans/modules/maven/execute/MavenExecutionTestBase.java
index 195a392..7543fc2 100644
--- a/java/maven/test/unit/src/org/netbeans/modules/maven/execute/MavenExecutionTestBase.java
+++ b/java/maven/test/unit/src/org/netbeans/modules/maven/execute/MavenExecutionTestBase.java
@@ -90,6 +90,7 @@ public class MavenExecutionTestBase extends NbTestCase {
     protected String mavenAppArgs = ""; // NOI18N
     protected Map<String, String> mavenExecutorDefines = new HashMap<>();
     protected Map<String, String> mavenExecutorRawDefines = new HashMap<>();
+    protected Map<String, String> mavenExecutorEnvironment = new HashMap<>();
     
     protected final InstanceContent actionData = new InstanceContent();
     protected final Lookup actionLookup = new AbstractLookup(actionData);
@@ -240,7 +241,7 @@ public class MavenExecutionTestBase extends NbTestCase {
      * Evaluates property references in arguments. Uses Maven evaluator for the project + properties defined in the 
      * ModelRunConfig (which are passed as -D to the maven executor). Collects all -D defines into {@link #mavenExecutorDefines}
      */
-    protected List<String> substituteProperties(List<String> args, Project p, Map<? extends String, ? extends String> properties) {
+    protected List<String> substituteProperties(List<String> args, Map<String, String> environment, Project p, Map<? extends String, ? extends String> properties) {
         Map<String, String> props = new HashMap<>(properties);
         for (int i = 0; i < args.size(); i++) {
             String a = args.get(i);
@@ -261,6 +262,7 @@ public class MavenExecutionTestBase extends NbTestCase {
             pE.setValue(interpolate(pE.getValue(), e, props));
         }
         mavenExecutorDefines = props;
+        mavenExecutorEnvironment = new HashMap<>(environment);
         return args;
     }
     
@@ -327,7 +329,7 @@ public class MavenExecutionTestBase extends NbTestCase {
         MavenCommandLineExecutor exec = new MavenCommandLineExecutor(cfg, InputOutput.NULL, null) {
             @Override
             int executeProcess(CommandLineOutputHandler out, ProcessBuilder builder, Consumer<Process> processSetter) throws IOException, InterruptedException {
-                List<String> args = substituteProperties(builder.command(), project, cfg.getProperties());
+                List<String> args = substituteProperties(builder.command(), builder.environment(), project, cfg.getProperties());
                 commandLineAcceptor.accept(args);
                 return 0;
             }
diff --git a/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/ExecutionEnvHelperTest.java b/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/ExecutionEnvHelperTest.java
index 796e2d3..9d53987 100644
--- a/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/ExecutionEnvHelperTest.java
+++ b/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/ExecutionEnvHelperTest.java
@@ -18,6 +18,7 @@
  */
 package org.netbeans.modules.maven.runjar;
 
+import java.io.File;
 import org.netbeans.modules.maven.execute.MavenExecutionTestBase;
 import java.io.StringReader;
 import java.util.Map;
@@ -63,6 +64,10 @@ public abstract class ExecutionEnvHelperTest extends MavenExecutionTestBase {
     
     protected void assertActionCustomVMProperties(String vmArg, String mainClass, String appArg) throws Exception {}
 
+    protected void assertActionWorkingDir(String workingDir) throws Exception {}
+
+    protected void assertActionEnvVariable(String varName, String varValue) throws Exception {}
+
     public static class NetBeans123Config extends ExecutionEnvHelperTest {
 
         public NetBeans123Config(String name) {
@@ -159,6 +164,16 @@ public abstract class ExecutionEnvHelperTest extends MavenExecutionTestBase {
         }
 
         @Override
+        protected void assertActionWorkingDir(String workingDir) throws Exception {
+            assertEquals(workingDir, mavenExecutorDefines.get(MavenExecuteUtils.RUN_WORKDIR));
+        }
+
+        @Override
+        protected void assertActionEnvVariable(String varName, String varValue) throws Exception {
+            assertEquals(varValue, mavenExecutorEnvironment.get(varName));
+        }
+
+        @Override
         protected String defaultCommandLineArgs() {
             return MavenExecuteUtils.DEFAULT_EXEC_ARGS_CLASSPATH2;
         }
@@ -463,6 +478,43 @@ public abstract class ExecutionEnvHelperTest extends MavenExecutionTestBase {
         assertTrue(mavenVmArgs.contains("-Dbar=foo"));
     }
 
+    /**
+     * Checks that pre-12.3 default actions will inject working directory from Lookup.
+     */
+    public void test123DefaultActionWithCWD() throws Exception {
+        initCustomizedProperties();
+        createNbActions(runP, debugP, profileP);
+        File wd = new File("WorkingDirectory");
+        ExplicitProcessParameters explicit = ExplicitProcessParameters.builder().
+                workingDirectory(wd).build();
+        actionData.add(explicit);
+        createPomWithArguments();
+        final Project project = ProjectManager.getDefault().findProject(pom.getParent());
+        loadActionMappings(project);
+        assertMavenRunAction(project, runMapping, "run", c -> {});
+        assertActionWorkingDir(wd.getAbsolutePath());
+    }
+
+    /**
+     * Checks that pre-12.3 default actions will inject environment variables from Lookup.
+     */
+    public void test123DefaultActionWithEnvVars() throws Exception {
+        initCustomizedProperties();
+        createNbActions(runP, debugP, profileP);
+        ExplicitProcessParameters explicit = ExplicitProcessParameters.builder().
+                environmentVariable("TEST_VAR1", "test value 1").
+                environmentVariable("TEST_VAR2", "test value 2").
+                environmentVariable("PATH", null).build();
+        actionData.add(explicit);
+        createPomWithArguments();
+        final Project project = ProjectManager.getDefault().findProject(pom.getParent());
+        loadActionMappings(project);
+        assertMavenRunAction(project, runMapping, "run", c -> {});
+        assertActionEnvVariable("TEST_VAR1", "test value 1");
+        assertActionEnvVariable("TEST_VAR2", "test value 2");
+        assertActionEnvVariable("PATH", null);
+    }
+
     private ExecutionEnvHelper load123ProjectExecutionHelper(boolean defaultProps) throws Exception {
         if (defaultProps) {
             initDefaultProperties();

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists