Repository: quarkusio/quarkus
Issue: Unable to copy .env file in dev mode

**Describe the bug**
The `.env` file for environment variables at project root can't be used on Windows 10.
When creating this file and running `mvnw quarkus:dev` it produces a warning in the log and the environment variables are not used by quarkus.
**Expected behavior**
I can define environment variables in `.env` file and use it in `application.properties` like this:
`greeting.message = ${MESSAGE}`

**Actual behavior**
A warning and exception is printed to the console:
```
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \\/ / / / _ | / _ \\/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\\ \\
--\\___\\_\\____/_/ |_/_/|_/_/|_|\\____/___/
2020-03-31 12:12:54,098 WARN  [io.qua.dev.DevModeMain] (main) Unable to copy .env file
2020-03-31 12:12:57,080 ERROR [io.qua.application] (main) Failed to start application: java.util.NoSuchElementException: Property MESSAGE not found
        at io.smallrye.config.SmallRyeConfig.propertyNotFound(SmallRyeConfig.java:209)
        at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:96)
        at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:80)
        at io.quarkus.runtime.configuration.ConfigExpander.accept(ConfigExpander.java:55)
        at io.quarkus.runtime.configuration.ConfigExpander.accept(ConfigExpander.java:15)
        at org.wildfly.common.expression.ExpressionNode.emit(ExpressionNode.java:42)
        at org.wildfly.common.expression.Expression.evaluateException(Expression.java:75)
        at org.wildfly.common.expression.Expression.evaluate(Expression.java:89)
        at io.quarkus.runtime.configuration.ExpandingConfigSource.expandValue(ExpandingConfigSource.java:101)
        at io.quarkus.runtime.configuration.ExpandingConfigSource.expand(ExpandingConfigSource.java:69)   
        at io.quarkus.runtime.configuration.ExpandingConfigSource.getValue(ExpandingConfigSource.java:47) 
        at io.quarkus.runtime.configuration.DeploymentProfileConfigSource.getValue(DeploymentProfileConfigSource.java:80)
        at io.smallrye.config.SmallRyeConfig.getRawValue(SmallRyeConfig.java:121)
        at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:84)
        at io.smallrye.config.SmallRyeConfig.getOptionalValue(SmallRyeConfig.java:131)
        at io.quarkus.arc.runtime.ConfigRecorder.validateConfigProperties(ConfigRecorder.java:35)
        at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties24.deploy_0(ConfigBuildStep$validateConfigProperties24.zig:120)
        at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties24.deploy(ConfigBuildStep$validateConfigProperties24.zig:36)
        at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:154)
        at io.quarkus.runtime.Application.start(Application.java:90)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at io.quarkus.runner.bootstrap.StartupActionImpl.run(StartupActionImpl.java:99)
        at io.quarkus.dev.IsolatedDevModeMain.firstStart(IsolatedDevModeMain.java:60)
        at io.quarkus.dev.IsolatedDevModeMain.accept(IsolatedDevModeMain.java:236)
        at io.quarkus.dev.IsolatedDevModeMain.accept(IsolatedDevModeMain.java:39)
        at io.quarkus.bootstrap.app.CuratedApplication.runInCl(CuratedApplication.java:131)
        at io.quarkus.bootstrap.app.CuratedApplication.runInAugmentClassLoader(CuratedApplication.java:84)
        at io.quarkus.dev.DevModeMain.start(DevModeMain.java:113)
        at io.quarkus.dev.DevModeMain.main(DevModeMain.java:54)

2020-03-31 12:12:57,111 ERROR [io.qua.dev.DevModeMain] (main) Failed to start Quarkus: java.lang.RuntimeException: Failed to start quarkus
        at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:232)
        at io.quarkus.runtime.Application.start(Application.java:90)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at io.quarkus.runner.bootstrap.StartupActionImpl.run(StartupActionImpl.java:99)
        at io.quarkus.dev.IsolatedDevModeMain.firstStart(IsolatedDevModeMain.java:60)
        at io.quarkus.dev.IsolatedDevModeMain.accept(IsolatedDevModeMain.java:236)
        at io.quarkus.dev.IsolatedDevModeMain.accept(IsolatedDevModeMain.java:39)
        at io.quarkus.bootstrap.app.CuratedApplication.runInCl(CuratedApplication.java:131)
        at io.quarkus.bootstrap.app.CuratedApplication.runInAugmentClassLoader(CuratedApplication.java:84)
        at io.quarkus.dev.DevModeMain.start(DevModeMain.java:113)
        at io.quarkus.dev.DevModeMain.main(DevModeMain.java:54)
Caused by: java.util.NoSuchElementException: Property MESSAGE not found
        at io.smallrye.config.SmallRyeConfig.propertyNotFound(SmallRyeConfig.java:209)
        at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:96)
        at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:80)
        at io.quarkus.runtime.configuration.ConfigExpander.accept(ConfigExpander.java:55)
        at io.quarkus.runtime.configuration.ConfigExpander.accept(ConfigExpander.java:15)
        at org.wildfly.common.expression.ExpressionNode.emit(ExpressionNode.java:42)
        at org.wildfly.common.expression.Expression.evaluateException(Expression.java:75)
        at org.wildfly.common.expression.Expression.evaluate(Expression.java:89)
        at io.quarkus.runtime.configuration.ExpandingConfigSource.expandValue(ExpandingConfigSource.java:101)
        at io.quarkus.runtime.configuration.ExpandingConfigSource.expand(ExpandingConfigSource.java:69)   
        at io.quarkus.runtime.configuration.ExpandingConfigSource.getValue(ExpandingConfigSource.java:47) 
        at io.quarkus.runtime.configuration.DeploymentProfileConfigSource.getValue(DeploymentProfileConfigSource.java:80)
        at io.smallrye.config.SmallRyeConfig.getRawValue(SmallRyeConfig.java:121)
        at io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:84)
        at io.smallrye.config.SmallRyeConfig.getOptionalValue(SmallRyeConfig.java:131)
        at io.quarkus.arc.runtime.ConfigRecorder.validateConfigProperties(ConfigRecorder.java:35)
        at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties24.deploy_0(ConfigBuildStep$validateConfigProperties24.zig:120)
        at io.quarkus.deployment.steps.ConfigBuildStep$validateConfigProperties24.deploy(ConfigBuildStep$validateConfigProperties24.zig:36)
        at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:154)
        ... 13 more
```

**To Reproduce**
Steps to reproduce the behavior:
1. `git clone https://github.com/quarkusio/quarkus-quickstarts.git` (tag: `1.3.1.Final`)
2. go to `config-quickstart` directory
3. add a `.env` file in the `config-quickstart` directory, see below for the content
4. modify `application.properties` like below
5. run `mvnw quarkus:dev`

**Configuration**
`application.properties`:
```properties
# Your configuration properties
greeting.message = ${MESSAGE}
greeting.name = quarkus
```
`.env`:
```
MESSAGE=hello
```
**Screenshots**
(If applicable, add screenshots to help explain your problem.)

**Environment (please complete the following information):**
 - Output of `uname -a` or `ver`: Microsoft Windows [Version 10.0.18363.720]
 - Output of `java -version`: 
   ```
   openjdk version "1.8.0_232"
   OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_232-b09)
   OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.232-b09, mixed mode)
   ```
 - GraalVM version (if different from Java): not installed
 - Quarkus version or git rev: 1.3.1.Final
 - Build tool (ie. output of `mvnw --version` or `gradlew --version`): 
   ```
   Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-27T17:06:16+02:00)
   Maven home: C:\\Users\\Leon\\.m2\\wrapper\\dists\\apache-maven-3.6.2-bin\\795eh28tki48bv3l67maojf0ra\\apache-maven-3.6.2
   Java version: 1.8.0_232, vendor: AdoptOpenJDK, runtime: C:\\Program Files\\AdoptOpenJDK\\jdk-8.0.232.09-hotspot\\jre
   Default locale: de_DE, platform encoding: Cp1252
   OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"
   ```

**Additional context**
Feature was added in #7519

Changed files:
core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java
core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java
core/deployment/src/main/java/io/quarkus/deployment/util/ProcessUtil.java
core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java
index 6771d0240c9..a5deda3c6e7 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java
@@ -1,14 +1,17 @@
 
 package io.quarkus.deployment.dev;
 
+import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.DataInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.ObjectInputStream;
+import java.io.PrintStream;
 import java.net.URI;
 import java.net.URL;
+import java.nio.file.FileSystemException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -16,6 +19,7 @@
 import java.util.Map;
 import java.util.Properties;
 
+import org.apache.commons.lang3.SystemUtils;
 import org.jboss.logging.Logger;
 
 import io.quarkus.bootstrap.app.AdditionalDependency;
@@ -23,6 +27,7 @@
 import io.quarkus.bootstrap.app.QuarkusBootstrap;
 import io.quarkus.bootstrap.model.AppArtifactKey;
 import io.quarkus.bootstrap.model.PathsCollection;
+import io.quarkus.deployment.util.ProcessUtil;
 import io.quarkus.dev.appstate.ApplicationStateNotification;
 
 /**
@@ -123,7 +128,7 @@ public void start() throws Exception {
                 }
             }
 
-            copyDotEnvFile();
+            linkDotEnvFile();
 
             Properties buildSystemProperties = new Properties();
             buildSystemProperties.putAll(context.getBuildSystemProperties());
@@ -140,10 +145,10 @@ public void start() throws Exception {
         }
     }
 
-    // copies the .env file to the directory where the process is running
+    // links the .env file to the directory where the process is running
     // this is done because for the .env file to take effect, it needs to be
     // in the same directory as the running process
-    private void copyDotEnvFile() {
+    private void linkDotEnvFile() {
         File projectDir = context.getProjectDir();
         if (projectDir == null) { // this is the case for QuarkusDevModeTest
             return;
@@ -157,13 +162,24 @@ private void copyDotEnvFile() {
 
         Path dotEnvPath = projectDir.toPath().resolve(".env");
         if (Files.exists(dotEnvPath)) {
+            Path link = currentDir.resolve(".env");
+            silentDeleteFile(link);
             try {
-                Path link = currentDir.resolve(".env");
-                silentDeleteFile(link);
                 // create a symlink to ensure that user updates to the file have the expected effect in dev-mode
-                Files.createSymbolicLink(link, dotEnvPath);
-            } catch (IOException e) {
-                log.warn("Unable to copy .env file", e);
+                try {
+                    Files.createSymbolicLink(link, dotEnvPath);
+                } catch (FileSystemException e) {
+                    // on Windows fall back to mklink if symlink cannot be created via Files API (due to insufficient permissions)
+                    // see https://github.com/quarkusio/quarkus/issues/8297
+                    if (SystemUtils.IS_OS_WINDOWS) {
+                        log.debug("Falling back to mklink on Windows after FileSystemException", e);
+                        makeHardLinkWindowsFallback(link, dotEnvPath);
+                    } else {
+                        throw e;
+                    }
+                }
+            } catch (IOException | InterruptedException e) {
+                log.warn("Unable to link .env file", e);
             }
         }
     }
@@ -176,6 +192,24 @@ private void silentDeleteFile(Path path) {
         }
     }
 
+    private void makeHardLinkWindowsFallback(Path link, Path dotEnvPath) throws IOException, InterruptedException {
+        Process process = new ProcessBuilder("cmd.exe", "/C", "mklink", "/H", link.toString(), dotEnvPath.toString())
+                .redirectOutput(new File("NUL"))
+                .redirectError(ProcessBuilder.Redirect.PIPE)
+                .start();
+        try {
+            ByteArrayOutputStream errStream = new ByteArrayOutputStream();
+            ProcessUtil.streamErrorTo(new PrintStream(errStream), process);
+            int exitValue = process.waitFor();
+            if (exitValue > 0) {
+                throw new IOException(
+                        "mklink /H execution failed with exit code " + exitValue + ": " + new String(errStream.toByteArray()));
+            }
+        } finally {
+            process.destroy();
+        }
+    }
+
     @Override
     public void close() throws IOException {
         if (realCloseable != null) {
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java
index 97ec754a588..1198ea1da15 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java
@@ -11,6 +11,7 @@
 import java.util.Optional;
 import java.util.function.BooleanSupplier;
 
+import org.apache.commons.lang3.SystemUtils;
 import org.jboss.logging.Logger;
 
 import io.quarkus.bootstrap.util.IoUtils;
@@ -237,7 +238,5 @@ public boolean getAsBoolean() {
     // copied from Java 9
     // TODO remove when we move to Java 11
 
-    private static final File NULL_FILE = new File(
-            (System.getProperty("os.name")
-                    .startsWith("Windows") ? "NUL" : "/dev/null"));
+    private static final File NULL_FILE = new File(SystemUtils.IS_OS_WINDOWS ? "NUL" : "/dev/null");
 }
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
index 9613f8fa3fb..048df634b36 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
@@ -23,7 +23,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -38,6 +37,7 @@
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
+import org.apache.commons.lang3.SystemUtils;
 import org.jboss.logging.Logger;
 
 import io.quarkus.bootstrap.BootstrapDependencyProcessingException;
@@ -131,8 +131,6 @@ public class JarResultBuildStep {
     public static final String QUARKUS = "quarkus";
     public static final String DEFAULT_FAST_JAR_DIRECTORY_NAME = "quarkus-app";
 
-    private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows");
-
     @BuildStep
     OutputTargetBuildItem outputTarget(BuildSystemTargetBuildItem bst, PackageConfig packageConfig) {
         String name = packageConfig.outputName.isPresent() ? packageConfig.outputName.get() : bst.getBaseName();
@@ -669,7 +667,7 @@ public NativeImageSourceJarBuildItem buildNativeImageJar(CurateOutcomeBuildItem
                 .map((s) -> new GeneratedClassBuildItem(true, s.getName(), s.getClassData()))
                 .collect(Collectors.toList()));
 
-        if (IS_WINDOWS) {
+        if (SystemUtils.IS_OS_WINDOWS) {
             log.warn("Uber JAR strategy is used for native image source JAR generation on Windows. This is done " +
                     "for the time being to work around a current GraalVM limitation on Windows concerning the " +
                     "maximum command length (see https://github.com/oracle/graal/issues/2387).");
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java
index abecf3d1f91..c1ddfd2fd4d 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java
@@ -15,13 +15,13 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import org.apache.commons.lang3.SystemUtils;
 import org.jboss.logging.Logger;
 
 import io.quarkus.bootstrap.util.IoUtils;
@@ -54,9 +54,6 @@ public class NativeImageBuildStep {
      */
     private static final String JAVA_HOME_ENV = "JAVA_HOME";
 
-    private static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("linux");
-    private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows");
-
     /**
      * The name of the environment variable containing the system path.
      */
@@ -97,13 +94,13 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa
             nativeImage = new ArrayList<>();
 
             String outputPath = outputDir.toAbsolutePath().toString();
-            if (IS_WINDOWS) {
+            if (SystemUtils.IS_OS_WINDOWS) {
                 outputPath = FileUtil.translateToVolumePath(outputPath);
             }
             Collections.addAll(nativeImage, containerRuntime, "run", "-v",
                     outputPath + ":" + CONTAINER_BUILD_VOLUME_PATH + ":z", "--env", "LANG=C");
 
-            if (IS_LINUX) {
+            if (SystemUtils.IS_OS_LINUX) {
                 if ("docker".equals(containerRuntime)) {
                     String uid = getLinuxID("-ur");
                     String gid = getLinuxID("-gr");
@@ -143,7 +140,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa
             }
 
         } else {
-            if (IS_LINUX) {
+            if (SystemUtils.IS_OS_LINUX) {
                 noPIE = detectNoPIE();
             }
 
@@ -312,7 +309,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa
                         + " Please consider removing this configuration key as it is ignored (JNI is always enabled) and it"
                         + " will be removed in a future Quarkus version.");
             }
-            if (!nativeConfig.enableServer && !IS_WINDOWS) {
+            if (!nativeConfig.enableServer && !SystemUtils.IS_OS_WINDOWS) {
                 command.add("--no-server");
             }
             if (nativeConfig.enableVmInspection) {
@@ -346,7 +343,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa
             if (exitCode != 0) {
                 throw imageGenerationFailed(exitCode, command);
             }
-            if (IS_WINDOWS && !(isContainerBuild)) {
+            if (SystemUtils.IS_OS_WINDOWS && !(isContainerBuild)) {
                 //once image is generated it gets added .exe on Windows
                 executableName = executableName + ".exe";
             }
@@ -398,7 +395,7 @@ private void handleAdditionalProperties(NativeConfig nativeConfig, List<String>
 
     private RuntimeException imageGenerationFailed(int exitValue, List<String> command) {
         if (exitValue == OOM_ERROR_VALUE) {
-            if (command.contains("docker") && !IS_LINUX) {
+            if (command.contains("docker") && !SystemUtils.IS_OS_LINUX) {
                 return new RuntimeException("Image generation failed. Exit code was " + exitValue
                         + " which indicates an out of memory error. The most likely cause is Docker not being given enough memory. Also consider increasing the Xmx value for native image generation by setting the \\""
                         + QUARKUS_XMX_PROPERTY + "\\" property");
@@ -423,7 +420,7 @@ private void checkGraalVMVersion(String version) {
     }
 
     private static File getNativeImageExecutable(Optional<String> graalVmHome, File javaHome, Map<String, String> env) {
-        String imageName = IS_WINDOWS ? "native-image.cmd" : "native-image";
+        String imageName = SystemUtils.IS_OS_WINDOWS ? "native-image.cmd" : "native-image";
         if (graalVmHome.isPresent()) {
             File file = Paths.get(graalVmHome.get(), "bin", imageName).toFile();
             if (file.exists()) {
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ProcessUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ProcessUtil.java
index d23cf7d139e..2f18f890adc 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/util/ProcessUtil.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ProcessUtil.java
@@ -105,8 +105,20 @@ public static void streamOutputToSysOut(final Process process) {
      * @param process The process whose STDERR needs to be streamed.
      */
     public static void streamErrorToSysErr(final Process process) {
+        streamErrorTo(System.err, process);
+    }
+
+    /**
+     * Streams the {@link Process process'} {@code STDERR} to the given
+     * {@code printStream}. This creates and starts a thread to stream the contents.
+     * The {@link Process} is expected to have been started in {@link java.lang.ProcessBuilder.Redirect#PIPE}
+     * mode
+     *
+     * @param process The process whose STDERR needs to be streamed.
+     */
+    public static void streamErrorTo(final PrintStream printStream, final Process process) {
         final InputStream processStdErr = process.getErrorStream();
-        final Thread t = new Thread(new Streamer(processStdErr, System.err));
+        final Thread t = new Thread(new Streamer(processStdErr, printStream));
         t.setName("Process stderr streamer");
         t.setDaemon(true);
         t.start();