`-E env` doesn't export empty environment variables on Windows

Hi,

Running cmake -E env NAME="" <program> gives different behavior when running on Linux vs Windows.

On Linux (what I’m used too) the environment variables is defined and contains an empty string, but on Windows the environment variable become undefined/deleted. In my case this is problematic as the third-party python script [1] that I use, has different behavior between when an environment variable is undefined or defined as a empty string.

The normal legacy behavior on Windows seems to be that the environment variable should be deleted when assigned to an empty string, however since .NET 9, this behavior has been changed [2].

My suggestion is to either change the behavior to allow empty environment variables or add a new option to allow it.

Best regards,
Devjoa

[1] GitHub - zephyrproject-rtos/Kconfiglib: Canonical upstream for https://pypi.org/project/kconfiglib/ (replaces inactive https://github.com/ulfalizer/Kconfiglib)

[2] Breaking change: Support for empty environment variables - .NET | Microsoft Learn

Starting with CMake 3.25, you can do cmake -E env –modify NAME=unset: <program>. That should work everywhere.

My understanding is that Windows doesn’t differentiate between an empty environment variable and an undefined environment variable. I believe setting an environment variable to an empty string is effectively the same as making it undefined. This has come up in a few other places from time to time, and I’m just relaying that information here. I don’t know the details or whether there has been a change in that behavior recently.

Link [2] in the OP mentions support for empty string environment values in .NET, so presumably the OS supports it now.

And I realize my suggestion is just doing what the current behavior is, so it isn’t actually helpful. Alas, CMake is using the system APIs to set environment variables; I wonder if CMake needs to adapt and call the routines documented in [2] instead of the current ones?

I am afraid that behavior is only available in .NET because it seems that environment variables are managed in the .NET process, not the OS itself.
Anyway, currently, CMake does not use .NET at all.

Ah. Well, that is quite unfortunate that there is now “yes, we support empty string envvar values” with the unstated subtext of “but executed processes will still see them as undefined”. In-process envvar communication is a horrible practice…why support something only good for that? But oh well.

As a non windows programmer I might be way off when trying to understand how Windows handle environment variables. I’ve done some testing with MSYS2 and it’s /usr/bin/envcommand is able to assign empty string variable and to my understanding .NET is not a MSYS2 dependency.

The SetEnvironmentVariable(winbase.h) documentation [3] it says that NULLas lpValue parameter will delete the environment variable, but nothing about empty strings. However, when I browse the CMake code I find SetEnvironmentVariableW in Utilities/cmlibuv/src/win/util.cand it doesn’t seem to make any distinguish between NULLand ””. Though I haven’t checked how the calling code is parsing the arguments.

I believe it should be possible to add, but I don’t know how… :frowning:

[3] SetEnvironmentVariable function (winbase.h) - Win32 apps | Microsoft Learn

I fired up Visual Studio on my Windows work computer and created the following patch as a proof of concept, and without .NET dependencies.

Running the patch with my local build cmake command gives me now an empty “HELLO” variable:
./cmake -E env HELLO= ./cmake -E environment

However, I assume that the correct way to fix the problem is to change cmSystemTools::PutEnv to use the uv_os_setenv instead of putenv, but the build dependencies was too complicated for me to do an easy fix.

Please consider to change this, even though it is a breaking change for windows.

diff --git a/Source/cmSystemTools.cxx b/Source/cmSystemTools.cxx
index 85aba246fd..8cae736b90 100644
--- a/Source/cmSystemTools.cxx
+++ b/Source/cmSystemTools.cxx
@@ -2271,14 +2271,15 @@ void cmSystemTools::EnvDiff::ApplyToCurrentEnv(std::ostringstream* measurement)
 {
   for (auto const& env_apply : diff) {
     if (env_apply.second) {
-      auto const env_update =
-        cmStrCat(env_apply.first, '=', *env_apply.second);
-      cmSystemTools::PutEnv(env_update);
+      uv_os_setenv(env_apply.first.c_str(), (*env_apply.second).c_str());
+
       if (measurement) {
+        auto const env_update =
+          cmStrCat(env_apply.first, '=', *env_apply.second);
         *measurement << env_update << std::endl;
       }
     } else {
-      cmSystemTools::UnsetEnv(env_apply.first.c_str());
+      uv_os_unsetenv(env_apply.first.c_str());
       if (measurement) {
         // Signify that this variable is being actively unset
         *measurement << '#' << env_apply.first << "=\n";

Thanks. There are many code paths in CMake that set/unset environment variables through the cmSystemTools::*Env helpers. Some of the code both implementing and calling them comes from KWSys, where we cannot add libuv as a dependency. It looks like libuv’s implementation just uses SetEnvironmentVariableW anyways. I’d welcome an update to KWSys to use that. Then I can import the change into CMake’s copy of KWSys.

After looking into this a bit further, changes made by SetEnvironmentVariableW affect the process’s environment but may not update the C runtime library’s copy used by getenv. We’d also need to port all environment lookups over to GetEnvironmentVariableW.

I can try to fix it in accordance to CMake design philosophy, even though I’m not fluent in the inner working of windows. :slight_smile:

Hi Brad,

I have created a patch that passes all tests and seems to work fine:

diff --git a/Source/kwsys/SystemTools.cxx b/Source/kwsys/SystemTools.cxx
index 3a2dc5a980..4c4821f132 100644
--- a/Source/kwsys/SystemTools.cxx
+++ b/Source/kwsys/SystemTools.cxx
@@ -750,26 +750,22 @@ static int kwsysUnPutEnv(std::string const& env)
 }
 
 #elif defined(_WIN32)
-/* putenv("A=") places "A=" in the environment, which is as close to
-   removal as we can get with the putenv API.  We have to leak the
-   most recent value placed in the environment for each variable name
-   on program exit in case exit routines access it.  */
-
 static kwsysEnvSet kwsysUnPutEnvSet;
 
 static int kwsysUnPutEnv(std::string const& env)
 {
   std::wstring wEnv = Encoding::ToWide(env);
   size_t const pos = wEnv.find('=');
-  size_t const len = pos == std::string::npos ? wEnv.size() : pos;
-  wEnv.resize(len + 1, L'=');
+  if (pos != std::string::npos) {
+    wEnv.resize(pos, L'\0');
+  }
   wchar_t* newEnv = _wcsdup(wEnv.c_str());
   if (!newEnv) {
     return -1;
   }
   kwsysEnvSet::Free oldEnv(kwsysUnPutEnvSet.Release(newEnv));
   kwsysUnPutEnvSet.insert(newEnv);
-  return _wputenv(newEnv);
+  return SetEnvironmentVariableW(newEnv, nullptr) ? 0 : -1;
 }
 
 #else
@@ -846,7 +842,7 @@ public:
   bool Put(char const* env)
   {
 #  if defined(_WIN32)
-    std::wstring const wEnv = Encoding::ToWide(env);
+    std::wstring wEnv = Encoding::ToWide(env);
     wchar_t* newEnv = _wcsdup(wEnv.c_str());
 #  else
     char* newEnv = strdup(env);
@@ -854,7 +850,14 @@ public:
     Free oldEnv(this->Release(newEnv));
     this->insert(newEnv);
 #  if defined(_WIN32)
-    return _wputenv(newEnv) == 0;
+    size_t const pos = wEnv.find('=');
+    if (pos == std::string::npos) {
+      return -1;
+    }
+    std::wstring wVal = wEnv.substr(pos+1);
+    wEnv.resize(pos, L'\0');
+
+    return SetEnvironmentVariableW(wEnv.c_str(), wVal.c_str());
 #  else
     return putenv(newEnv) == 0;
 #  endif



or pull it from my git repository [ GitHub - devjoa/CMake at fix-windows-blank-environment-variable-problem ]

However, when searching the code base I found the following lines that claims they are deleting the following environment variables, i.e. they may need to be updated to use UnPutEnv due to the changed behavior?

Source/CPack/cmCPackGenerator.cxx:249:    cmSystemTools::PutEnv("DESTDIR=");
Source/CPack/cmCPackGenerator.cxx:322:    cmSystemTools::PutEnv("DESTDIR=");
Source/cmake.cxx:3024:    cmSystemTools::PutEnv("MAKEFLAGS=");

Thanks. Please open a merge request directly to KWSys, which I linked above. We can review details of the patch there.

On the CMake side, once we update to a KWSys with your changes, then you could open a new MR there to replace PutEnv("VAR=") with a proper unset call.

OK, done

https://gitlab.kitware.com/utils/kwsys/-/merge_requests/349

This is now CMake Issue 27285.