Backport using the example of Node.js v22 and Windows 7

What to do if you need to run modern software in an outdated environment? Let's talk about the process “porting back» latest version Node.js to the old one Windows 7.

“Hero screen”: Windows 7 and running Node.js v22 – running below "Angular boilerplate" project on a freshly compiled version

“Hero screen”: Windows 7 and running Node.js v22 – below the “Angular boilerplate” project is running on a freshly compiled version

While “somewhere high in the clouds,” neural networks led by Elon Musk are roaming the expanses of sunny California, and all decent society is waiting for the arrival of the singularity and new version of iOS – on our sinful Earth, quite ordinary factories with factories still stand and operate. Where it is normal to use equipment together with control software for decades.

As a result of this approach, at every large industrial enterprise, a whole ecosystemrunning on outdated (by the standards of the rest of the IT industry) software and depending on it.

With all this I am simply answering in advance the questions from the series “why is this needed” and “who needs this” – now you also know that even in ultra-modern production facilities there is always a dark basement in which there is a dusty machine running Windows 2000 (at best ), without which everything will burn and melt.

So the task interactions with legacy environments is unlikely to lose its relevance in the foreseeable future, at least until the onset of the singularity and the final victory of robots.

We also regularly receive similar tasks, based on which this article was written.

IN first versionwritten back in January 2024, we ported Node.js 20, which was then the latest LTS version (i.e., with the longest support cycle).

Six months later, and due to requests for further updates, 22.3.0 was already ported, primarily to test out possible future problems in the next LTS.

Node.js and Windows 7

Official support for Windows 7 in the project ended back in 2019:

With issues like 20348 being closed as wontfix, I dont think its fair to say that Node supports Windows 7 anymore, as the experience with the default terminal is so bad as to be unusable. Node now requires Windows 8 or Windows 10, whatever the case is.

The latest available version for the outdated “seven” is 12.22.7which is no longer enough to launch and build modern Node.js projects.

If you try to download and run a more recent version, you will see this message:

Standard installer on top and our custom assembly on bottom. "Test mode" - an echo of another driver development project, someday I’ll tell you about it.

Standard installer on top and our custom assembly on bottom. “Test mode” is an echo of another driver development project, I’ll tell you about that someday.

Unfortunately, the official build of Node.js for Windows no longer works, sending you towards the update site.

Some technical details

Node.js – quite old cross-platform project, and Windows was not the main platform for it for a long time.

From the point of view of the backporting problem, this means two things:

  • WinAPI call locations isolated and placed in separate files;

  • restrictions on the supported OS are mostly artificial and the main code of the project is not tied to any specific OS, much less its version.

But of course, everything is not so simple and there will be places where you have to use your head and even think a little.

The project itself is written in C++ (and, as expected, some pure C), it also actively uses Python for assembly – this is the first serious problem.

The fact is that Latest Python builds for Windows also do not support Windows 7:

Note that Python 3.12.1 cannot be used on Windows 7 or earlier.

In order not to bother with assembling Python from source code, a ready-made third-party backport was taken from here. The latest one is suitable for building Node.js version 3.9 branches.

Development environment

Of course, for such a popular project, the deployment of the development environment is fully automated:

A Boxstarter script can be used for easy setup of Windows systems with all the required prerequisites for Node.js development. This script will install the following Chocolatey packages:

And for regular development on a supported OS, it’s definitely worth using these tools. But since we do backport – all this automation will not work and will send you in the same way forest towards updates.

So, alas, you will have to deploy the entire development environment yourself, just like in the old days.

Python

Download and install the unofficial assembly Python 3.9 for Windows 7it’s better to go to a simpler folder – without spaces or Unicode characters in the path.

We make sure that python.exe is located in the PATH environment variable:

To build the 22nd version of Node.js, the same version of Python was used, don’t be alarmed.

To build version 22 of Node.js, the same version of Python was used, don’t be alarmed.

NASM

Suddenly it turned out that Node.js has inserts in pure assembler, so to build you need to install NASM. I used last (no longer available) version 2.16.02rc7, with official websitewhich was carried out build version 20 in January.

The installer runs successfully on Windows 7, but unfortunately does not add applications from the NASM set to the environment variable.

Therefore, after installation, you need to add the NASM folder to the PATH variable:

Visual Studio

Finally the last one, but the biggest The problem is the C++ compiler itself. You will need to download and install Visual Studio 2019, the latest version available for Windows 7. also supported by Node.js build scripts (not anymore):

In version 22 of Node.js, support for all legacy versions of Visual Studio except the latest was completely removed from the build script.

Unfortunately, Microsoft really doesn’t like it when outdated versions of its products are used, so finding a link to download the 2019 version of Visual Studio is another quest.

I managed to find it accidentally V this articleby downloading the “professional” version, direct link to download the installer, but without guarantees for how long it will remain relevant.

I also note that it is worth downloading and installing development environment and not just “build tools”, since you will need to edit the Node.js source code – doing this in a bare notepad is, to put it mildly, not very convenient.

This is what the installed version of Visual Studio looks like:

Please note that Visual Studio 2022 can also be installed on Windows 7, but will not function correctly.  This is such a paradox.

Please be aware that the 2022 version of Visual Studio can also be installed on Windows 7 and still function correctly will not. This is such a paradox.

You will need to install the entire packages marked in red:

Please also keep in mind that installation will take ~22GB disk space, more ~15GB will take a local build of Node.js.

Assembly

The Node.js source code can be downloaded from the repository Github or download one of the release archives with sources. The second option is faster, so I used it by downloading archive with sources from official website.

We unpack the archive, for example using 7Zip to a directory without spaces or Unicode characters and run the build through the script vcbuild.bat fundamentally:

Version 22 is no different in terms of assembly, so the screenshot remains the same

Version 22 is no different in terms of assembly, so the screenshot remains the same

Of course the first build will fallbut not at once:

we must make sure that all the necessary tools for assembly are found and identified – this information will be displayed at the very beginning of the assembly.

Only after making sure that everything we need has been found can we move on to editing the sources.

Patches

First of all, we disable the obvious stub that prevents the use of Node.js on outdated versions of Windows – it is shown already disabled in the first screenshot in the article.

File src/node_main.ccwe need lines ~34-50:

int wmain(int argc, wchar_t* wargv[]) {
  // Windows Server 2012 (not R2) is supported until 10/10/2023, so we allow it
  // to run in the experimental support tier.
  char buf[SKIP_CHECK_STRLEN + 1];
  if (!IsWindows8Point1OrGreater() &&
      !(IsWindowsServer() && IsWindows8OrGreater()) &&
      (GetEnvironmentVariableA(SKIP_CHECK_VAR, buf, sizeof(buf)) !=
           SKIP_CHECK_STRLEN ||
       strncmp(buf, SKIP_CHECK_VALUE, SKIP_CHECK_STRLEN) != 0)) {
    fprintf(stderr, "Node.js is only supported on Windows 8.1, Windows "
                    "Server 2012 R2, or higher.\n"
                    "Setting the " SKIP_CHECK_VAR " environment variable "
                    "to 1 skips this\ncheck, but Node.js might not execute "
                    "correctly. Any issues encountered on\nunsupported "
                    "platforms will not be fixed.");
    exit(ERROR_EXE_MACHINE_TYPE_MISMATCH);
}

This entire block needs to be deleted or commented out entirely.

In fact, this check does not interfere with the build process itself, but it will prevent the assembled version from running later.

Next stop – file deps/uv/src/win/util.che is big and scary because it contains an integration layer with Windows OS and WinAPI calls.

This is where the fun begins, as Node.js developers have started using WinAPI features only available in the latest versions of Windows.

In order to get around this and make the new software work on the old OS, there are only two solutions:

  1. emulate an unavailable function,

  2. use its available analogue.

The second is much simpler, and since the Node.js project has not managed to stray very far from its cross-platform foundations, such a replacement of one WinAPI call with another looks like an easy solution to the problem.

The first stop on the path to a successful build is function uv_os_gethostname(line ~1531) which uses the new WinAPI function GetHostNameW:

The GetHostNameW function retrieves the standard host name for the local computer as a Unicode string.

Which unfortunately did not exist in Windows 7:

Windows 8.1 and Windows Server 2012 R2: This function is supported for Windows Store apps on Windows 8.1, Windows Server 2012 R2, and later.

But everything is not so bad, since there were patches from other open projects that replaced this function with a standard POSIX analogue gethostnamefor example here such.

So the final fix will be a cakewalk:

int uv_os_gethostname(char* buffer, size_t* size) {
  //WCHAR buf[UV_MAXHOSTNAMESIZE];
  char buf[UV_MAXHOSTNAMESIZE];
size_t len;
//char* utf8_str;
//int convert_result;
if (buffer == NULL || size == NULL || *size == 0)
return UV_EINVAL;
uv__once_init(); /* Initialize winsock */
if (pGetHostNameW == NULL)
return UV_ENOSYS;
//if (pGetHostNameW(buf, UV_MAXHOSTNAMESIZE) != 0)
if (gethostname(buf, sizeof(buf)) != 0)
return uv_translate_sys_error(WSAGetLastError());
// convert_result = uv__convert_utf16_to_utf8(buf, -1, &utf8_str);
buf[sizeof(buf) - 1] = '\0'; /* Null terminate, just to be safe. */
len = strlen(buf);
// if (convert_result != 0)
//   return convert_result;
// len = strlen(utf8_str);
if (len >= *size) {
*size = len + 1;
//  uv__free(utf8_str);
return UV_ENOBUFS;
}
//memcpy(buffer, utf8_str, len + 1);
//uv__free(utf8_str);
memcpy(buffer, buf, len + 1);
*size = len;
return 0;
}

You can see it in diff form here here.

As you can see, the whole rework consists of replacing the WinAPI function call and using slightly different data structures for work.

The next problematic function is uv_clock_gettime (line ~509), which also uses the new WinAPI function GetSystemTimePreciseAsFileTime:

The GetSystemTimePreciseAsFileTime function retrieves the current system date and time with the highest possible level of precision (<1us). The retrieved information is in Coordinated Universal Time (UTC) format.

Not available in legacy Windows 7:

Minimum supported client Windows 8 [desktop apps | UWP apps]

Minimum supported server Windows Server 2012 [desktop apps | UWP apps]

Fortunately for us, there is a simple solution here in the form of replacing the call with GetSystemTimeAsFileTime:

int uv_clock_gettime(uv_clock_id clock_id, uv_timespec64_t* ts) {
  FILETIME ft;
  int64_t t;
if (ts == NULL)
return UV_EFAULT;
switch (clock_id) {
case UV_CLOCK_MONOTONIC:
uv__once_init();
t = uv__hrtime(UV__NANOSEC);
ts->tv_sec = t / 1000000000;
ts->tv_nsec = t % 1000000000;
return 0;
case UV_CLOCK_REALTIME:
GetSystemTimeAsFileTime(&ft);
//	  GetSystemTimePreciseAsFileTime(&ft);
/* In 100-nanosecond increments from 1601-01-01 UTC because why not? /
t = (int64_t) ft.dwHighDateTime << 32 | ft.dwLowDateTime;
/ Convert to UNIX epoch, 1970-01-01. Still in 100 ns increments. /
t -= 116444736000000000ll;
/ Now convert to seconds and nanoseconds. /
ts->tv_sec = t / 10000000;
ts->tv_nsec = t % 10000000  100;
return 0;
}
return UV_EINVAL;
}

To even greater joy, it turned out that the older function uses exactly the same data structure as an argument, so the whole edit consists only of replacing calls:

GetSystemTimeAsFileTime(&ft);
//GetSystemTimePreciseAsFileTime(&ft);

Finally, the last problem area that appeared in the new 22nd version of Node.js is this place in the file deps\v8\src\maglev maglev-assembler-inl.h (line ~490):

template <typename Descriptor, typename... Args>
void PushArgumentsForBuiltin(MaglevAssembler* masm, std::tuple<Args...> args) {
  std::apply(
      [&](auto&&... stack_args) {
        if (Descriptor::kStackArgumentOrder == StackArgumentOrder::kDefault) {
          masm->Push(std::forward<decltype(stack_args)>(stack_args)...);
        } else {
          masm->PushReverse(std::forward<decltype(stack_args)>(stack_args)...);
        }
      },
      args);
}

For some reason the compiler complains about kStackArgumentOrder – it can’t find it.

The problem is that the project MaglevThis optimizing compilerrunning inside the engine V8, on which Node.js is based. This is, to put it mildly, a complicated thing, and it will probably take you to fully understand how it works. a few years pure time and meditation.

Which of course I don't have. Therefore, it was decided to go a different route – to see which other open projects are using this Maglev.

Found out quickly enough here's a commit in the source code of the QtWebEngine engine, which uses V8 and Chromium internally for work.

According to this commit, the Descriptor class was changed to Descriptor2 so that the resulting code would look like this:

template <typename Descriptor2, typename... Args>
void PushArgumentsForBuiltin(MaglevAssembler* masm, std::tuple<Args...> args) {
  std::apply(
      [&](auto&&... stack_args) {
        if (Descriptor2::kStackArgumentOrder == StackArgumentOrder::kDefault) {
          masm->Push(std::forward<decltype(stack_args)>(stack_args)...);
        } else {
          masm->PushReverse(std::forward<decltype(stack_args)>(stack_args)...);
        }
      },
      args);
}

Unfortunately, it was not possible to establish any details about where exactly these ears problems, but the solution itself turned out to be quite working:

Maglev was successfully assembled, along with all its tests, no errors were noticed.

Assembly

Changes made to the code enough for work not anymore:

In the Node.js v22 build script (as opposed to the 20th LTS) deleted support for outdated versions of Visual Studio, so before running the script, you need to return the section responsible for building on the old 2019 version:

@rem Look for Visual Studio 2019
:vs-set-2019
if defined target_env if "%target_env%" NEQ "vs2019" goto msbuild-not-found
echo Looking for Visual Studio 2019
@rem VCINSTALLDIR may be set if run from a VS Command Prompt and needs to be
@rem cleared first as vswhere_usability_wrapper.cmd doesn't when it fails to
@rem detect the version searched for
if not defined target_env set "VCINSTALLDIR="
call tools\msvs\vswhere_usability_wrapper.cmd "[16.0,17.0)" %target_arch% "prerelease"
if "_%VCINSTALLDIR%_" == "__" goto msbuild-not-found
@rem check if VS2019 is already setup, and for the requested arch
if "_%VisualStudioVersion%_" == "_16.0_" if "_%VSCMD_ARG_TGT_ARCH%_"=="_%target_arch%_" goto found_vs2019
@rem need to clear VSINSTALLDIR for vcvarsall to work as expected
set "VSINSTALLDIR="
@rem prevent VsDevCmd.bat from changing the current working directory
set "VSCMD_START_DIR=%CD%"
set vcvars_call="%VCINSTALLDIR%\Auxiliary\Build\vcvarsall.bat" %vcvarsall_arg%
echo calling: %vcvars_call%
call %vcvars_call%
if errorlevel 1 goto msbuild-not-found
if defined DEBUG_HELPER @ECHO ON
:found_vs2019
echo Found MSVS version %VisualStudioVersion%
set GYP_MSVS_VERSION=2019
set PLATFORM_TOOLSET=v142
goto msbuild-found

Then we add an unconditional jump to the section for 2022:

@rem Look for Visual Studio 2022
:vs-set-2022
::if defined target_env if "%target_env%" NEQ "vs2022" 
goto vs-set-2019

Call goto vs-set-2019 will immediately go to the block we added, without trying to search for the 2022 version of Visual Studio.

After all the edits, run the build:

vcbuild.bat full-icu

Where is the argument full-icu indicates support for all locales.

The build will take quite a long time (~2 hours in my virtual machine, but of course this depends on the hardware), and version 22 will take significantly longer to build than 20 LTS. Judging by the build log, this is due to the increased number of autotests.

If everything ends successfully – in the catalog out\Release the finished build will appear. To check, run:

Release\node.exe -e "console.log('Hello from Node.js', process.version)"

The Javascript code should execute and the version of the newly built Node.js should be displayed.

But that's not all, because after the build there will be no package manager NPM. For it to appear, you need to collect the final archive – the same as you downloaded from the official website.

This is done with the command:

vcbuild.bat package

Then in the folder Release another directory will appear with the name of the release, into which the launch script will be added npm.cmd.

This directory contains the final Node.js build that you can use to build and run your modern projects.

The archive with the distribution kit is collected using 7zipso the path to it should be in the PATH variable.

Performance tests

Of course the most stubborn and suspicious experienced readers who have many years of experience in development behind them and understand the dangers of “easily replacing” one WinAPI call with another will immediately doubted — will such a “homemade” product work on real projects and in combat conditions?

We were also interested in this, so the first thing (after successful assembly) we took “bold” boilerplate on Angular 16 as a test project, assembled and launched.

It looks like this:

As you can see, the local server is running in development mode with HMR (background recompilation when changes) – the most resource-intensive launch option.

Disclaimer #1:

Of course, one such launch few for a serious assessment, so a whole set of autotests was created that completely emulates the customer’s environment and all the options for using Node.js – no problems have been found so far.

Disclaimer #2:

However, this does not guarantee that you personally will not have problems – the environment and use cases can vary greatly from project to project. For this reason, the above – not a guide to action and there is no reason for the immediate implementation of such a solution in the future. Especially if you have dangerous production, aviation or medical software.

If you want to do this, please spend 90% of your budget on complex integration testing, besides work on the assembly itself with patches.

0x08 Software

We are a small team of IT industry veterans, we create and develop a wide variety of software; our software automates business processes on three continents, in a wide variety of industries and conditions.

Let's revive long dead, fixing something that never worked and create impossible — then we talk about it in our articles.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *