Бекпорт на примере Node.js v22 и Windows 7

Что делать если надо запустить современный софт в устаревшем окружении? Рассказываем о процессе «портирования назад» последней версии Node.js на старую Windows 7.

«Hero screen»: Windows 7 и работающая Node.js v22 — внизу запущен

«Hero screen»: Windows 7 и работающая Node.js v22 — внизу запущен «Angular boilerplate» проект на свежесобранной версии

В то время как «где‑то там высоко в облаках» нейросети во главе с Илоном Маском бороздят просторы солнечной Калифорнии, а все приличное общество ждет прихода сингулярности и новой версии iOS — на нашей грешной Земле все также стоят и работают вполне обычные фабрики с заводами. На которых является нормой использовать оборудование вместе с управляющим софтом десятилетиями.

В результате такого подхода на каждом крупном промышленном предприятии живет и процветает целая экосистема, работающая на устаревшем (по меркам остальной ИТ‑индустрии) ПО и от него же зависящая.

Всем этим я просто заранее отвечаю на вопросы из серии «зачем это надо» и «кому это нужно» — теперь вы тоже знаете, что даже на суперсовременных производствах обязательно есть темный подвал, в котором стоит пыльный станок под управлением Windows 2000 (в лучшем случае), без которого все сгорит и расплавится.

Так что задача взаимодействия с устаревшим окружением врядли потеряет свою актуальность в обозримом будущем, по крайней мере до наступления сингулярности и окончательной победы роботов.

Нам тоже регулярно поступают подобные задачи, по мотивам выполнения которых и написана эта статья.

В первой версии, написанной еще в январе 2024 го года мы портировали 20ю Node.js, которая тогда являлась последней LTS‑версией (т.е. с самым длинным циклом поддержки).

Спустя полгода и ввиду пожеланий по дальнейшему обновлению — портировали уже 22.3.0, в первую очередь для обкатки возможных будущих проблем в следующем LTS.

Node.js и Windows 7

Официальная поддержка Windows 7 в проекте закончилась еще в 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.

Последняя доступная версия для устаревшей «семерки» это 12.22.7, которой уже не хватает для запуска и сборки современных проектов на Node.js.

Если же вы попробуете скачать и запустить более свежую версию — увидите вот такое сообщение:

Стандартный инсталлятор сверху и наша нестандартная сборка снизу.

Стандартный инсталлятор сверху и наша нестандартная сборка снизу. «Тестовый режим» — отголосок другого проекта по разработке драйверов, как‑нибудь расскажу и о нем.

Увы, но официальная сборка Node.js для Windows больше не работает, послав вас в сторону сайта с обновлениями.

Немного технических деталей

Node.js — достаточно старый кроссплатформенный проект, причем Windows долгое время не являлась для него основной платформой.

С точки зрения задачи бекпортирования это означает две вещи:

  • места вызовов WinAPI изолированы и вынесены в отдельные файлы;

  • ограничения на поддерживаемую ОС по большей части искусственны, а основной код проекта не имеет привязки к какой-либо конкретной ОС и тем более ее версии.

Но разумеется все не настолько просто и будут места где придется включать голову и даже немного ей думать.

Сам проект написан на C++ (и ожидаемо немного чистого Си), еще в нем активно используется Python для сборки — это первая серьезная проблема.

Дело в том что последние версии сборок Python для Windows также не поддерживают Windows 7:

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

Чтобы не заморачиваться сборкой еще и Python из исходников — был взят готовый сторонний бекпорт отсюда. Для сборки Node.js подойдет последняя версия 3.9 ветки.

Окружение разработки

Конечно же у столь популярного проекта развертывание окружения для разработки полностью автоматизировано:

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:

И для обычной разработки на поддерживаемой ОС точно стоит использовать именно эти инструменты. Но поскольку мы делаем бекпорт — вся эта автоматизация не сработает и точно также пошлет вас лесом в сторону обновлений.

Так что увы, но придется разворачивать все окружение для разработки своими руками, как в былые времена.

Python

Скачиваем и устанавливаем неофициальную сборку Python 3.9 для Windows 7, лучше в папку попроще — без пробелов и символов юникода в пути.

Убеждаемся что python.exe находится в переменной окружения PATH:

Для сборки 22й версии Node.js использовалась эта же версия Python, не пугайтесь.

Для сборки 22й версии Node.js использовалась эта же версия Python, не пугайтесь.

NASM

Внезапно оказалось что в Node.js есть вставки на чистом ассемблере, поэтому для сборки нужно установить NASM. Я использовал последнюю (уже нет) версию 2.16.02rc7, с официального сайта, которой проводилась сборка 20й версии в январе.

Инсталлятор успешно отрабатывает на Windows 7, но к сожалению не добавляет приложения из набора NASM в переменную окружения.

Поэтому после установки необходимо добавить папку с NASM в переменную PATH:

5ae9c31819dbd076903f2f53a7ac20f8.png

Visual Studio

Наконец последней, но самой большой проблемой является собственно компилятор C++. Вам нужно будет скачать и установить среду разработки Visual Studio 2019 — последнюю доступную версию для Windows 7, еще поддерживаемую скриптами сборки Node.js (уже нет):

В 22й версии Node.js из скрипта сборки полностью удалили поддержку всех устаревших версий Visual Studio кроме последней.

К сожалению Microsoft очень не любит когда используют устаревшие версии ее продуктов, поэтому поиск ссылки на скачивание 2019й версии Visual Studio является еще тем квестом.

Мне удалось ее обнаружить случайно в этой статье, скачав версию «professional», прямая ссылка для скачивания инсталлятора, но без гарантий как долго она будет оставаться актуальной.

Также отмечу, что стоит скачать и установить именно среду разработки, а не просто «build tools», поскольку нужно будет редактировать исходный код Node.js — делать это в голом «блокноте» мягко говоря не очень удобно.

Вот так выглядит установленная версия Visual Studio:

Имейте ввиду что версия Visual Studio 2022 также может быть установлена на Windows 7, но функционировать правильно не будет. Вот такой парадокс.

Имейте ввиду что версия Visual Studio 2022 также может быть установлена на Windows 7, но функционировать правильно не будет. Вот такой парадокс.

Нужно будет установить отмеченные красным пакеты целиком:

f726b8d4fb5fe8be2f520f74cdac89ec.png

Учитывайте также что установка займет ~22Гб места на диске, еще ~15Гб займет локальная сборка Node.js.

Сборка

Исходный код Node.js можно забрать из репозитория Github или скачать один из релизных архивов с исходниками. Второй вариант — быстрее, поэтому я использовал его, скачав архив с исходниками с официального сайта.

Распаковываем архив, например с помощью 7Zip в каталог без пробелов и символов юникода и запускаем сборку через скрипт vcbuild.bat в корне:

22я версия в плане сборки не отличается, поэтому скриншот остался старый

22я версия в плане сборки не отличается, поэтому скриншот остался старый

Разумеется первая сборка упадет, но не сразу:

мы должны убедиться что нашлись и определились все необходимые инструменты для сборки — эта информация будет отображена в самом начале сборки.

Только убедившись что все необходимое нашлось, переходим к редактированию исходников.

Патчи

Первым делом отключаем очевидную заглушку, которая не дает использовать Node.js на устаревших версиях Windows — она в уже отключенном виде показана на первом скриншоте в статье.

Файл src/node_main.cc, нам нужны строки ~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);
}

Весь этот блок необходимо удалить или закомментировать целиком.

Вообще-то самому процессу сборки эта проверка не мешает, зато не даст потом запустить собранную версию.

Следующая остановка — файл deps/uv/src/win/util.c, он большой и страшный поскольку содержит слой интеграции с ОС Windows и вызовы WinAPI.

Вот тут и начинается безудержное веселье, поскольку разработчики Node.js начали использовать функции WinAPI доступные только в последних версиях Windows.

Для того чтобы это обойти и заставить работать новый софт на старой ОС существует всего два решения:

  1. эмулировать недоступную функцию,

  2. использовать ее доступный аналог.

Второе сильно проще и поскольку проект Node.js не успел сильно далеко убежать от своих кроссплатформенных основ — такая замена одного вызова WinAPI на другой выглядит легким решением проблемы.

Первая остановка на пути к успешной сборке — функция uv_os_gethostname, (строка ~1531) которая использует новую функцию WinAPI GetHostNameW:

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

Которая к сожалению не существовала в 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.

Но все не так плохо, поскольку нашлись патчи из других открытых проектов, заменяющие эту функцию на стандартный POSIX-аналог gethostname, например вот такой.

Поэтому итоговое исправление будет лишь легкой прогулкой:

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;
}

В виде diff можно посмотреть вот тут.

Как видите вся переделка заключается в замене вызова функции WinAPI и использовании немного других структур данных для работы.

Следующая проблемная функция это uv_clock_gettime (строка ~509), в которой также используется новая функция WinAPI 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.

Недоступная в устаревшей Windows 7:

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

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

К нашему счастью и тут есть простое решение в виде замены вызова на 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;
}

К еще большей радости оказалось, что более старая функция использует точно такую же структуру данных в качестве аргумента, поэтому вся правка заключается только в замене вызовов:

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

Наконец последним проблемным участком, появившимся в новой 22й версии Node.js является вот это место в файле deps\v8\src\maglev maglev-assembler-inl.h (строка ~490):

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

Компилятор почему-то ругается на kStackArgumentOrder — не может его найти.

Проблема заключается в том что проект Maglev, это оптимизирующий компилятор, работающий внутри движка V8, на котором и основан Node.js. Это мягко говоря непростая штука, полностью разобраться в работе которой займет наверное пару лет чистого времени и медитаций.

Которых у меня разумеется нет. Поэтому было решено пойти другим путем — посмотреть кто еще из открытых проектов использует этот Maglev.

Достаточно быстро обнаружился вот такой коммит в исходниках движка QtWebEngine, использующего внутри V8 и Chromium для работы.

Согласно этому коммиту, был изменен класс Descriptor на Descriptor2, чтобы итоговый код выглядел следующим образом:

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

К сожалению не удалось установить каких‑либо деталей и подробностей — откуда именно вылезли эти уши проблемы, но само решение оказалось вполне рабочим:

Maglev успешно собрался, вместе со всеми своими тестами, каких-либо ошибок замечено не было.

Сборка

Внесенных в код изменений достаточно для работы уже нет:

В скрипте сборки Node.js v22 (в отличие от 20й LTS) удалили поддержку устаревших версий Visual Studio, поэтому прежде чем запускать скрипт — необходимо вернуть на место секцию, отвечающую за сборку на старой 2019й версии:

@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

Затем добавляем безусловный переход в секцию для 2022:

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

Вызов goto vs-set-2019 сразу сделает переход в добавленный нами блок, без попытки поиска 2022й версии Visual Studio.

После всех правок запускаем сборку:

vcbuild.bat full-icu

Где аргумент full-icu указывает на поддержку всех локалей.

Сборка будет идти достаточно долго (~2ч в моей виртуальной машине, но разумеется это зависит от оборудования), причем 22я версия собирается ощутимо дольше чем 20 LTS. Судя по логу сборки — из‑за выросшего количества автотестов.

Если все закончится успешно — в каталоге out\Release появится готовый билд. Для проверки запустите:

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

Должно произойти выполнение Javascript-кода и отобразиться версия только что собранного Node.js.

Но это еще не все, поскольку после сборки не будет пакетного менеджера NPM. Чтобы он появился, необходимо собрать финальный архив — такой же как вы скачивали с официального сайта.

Делается это командой:

vcbuild.bat package

После чего в папке Release появится еще один каталог с названием релиза, в который и будет добавлен скрипт для запуска npm.cmd.

Этот каталог содержит финальную сборку Node.js, которую можно использовать для сборки и запуска ваших современных проектов.

Архив с дистрибутивом собирается с помощью 7zip, поэтому путь к нему должен быть в переменной PATH.

Тесты работоспособности

Разумеется самые упертые и подозрительные опытные из читателей, имеющие за плечами много лет практики в разработке и понимающие чем может грозить «легкая замена» одного вызова WinAPI на другое, сразу же усомнились —, а будет ли такой «самопал» работать на реальных проектах и в боевых условиях?

Нам это тоже было интересно, поэтому первым (после успешной сборки) делом мы взяли «жирный» boilerplate на Angular 16 в качестве тестового проекта, собрали и запустили.

Выглядит это вот так:

06cba2af10afdeb46b6ee32a720c80f4.png

Как видите запущен локальный сервер в режиме разработки с HMR (фоновой перекомпиляцией при изменениях) — самый ресурсоемкий вариант запуска.

Disclaimer #1:

Разумеется одного такого запуска мало для серьезной оценки, поэтому был создан целый набор автотестов, полностью эмулирующий окружение заказчика и все варианты использования им Node.js — каких‑либо проблем пока не было обнаружено.

Disclaimer #2:

Тем не менее это не гарантирует что проблем не будет лично у вас — окружение и кейсы использования могут очень сильно отличаться от проекта к проекту. По этой причине описанное выше — не руководство к действию и не повод к немедленному внедрению такого решения в прод. Особенно если у вас опасное производство, авиационное или медицинское ПО.

Хотите таким заниматься — извольте 90% бюджета потратить на сложное интеграционное тестирование, помимо работ по самой сборке с патчами.

0×08 Software

Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.

Оживляем давно умершее,  чиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.

© Habrahabr.ru