Бекпорт: запускаем Node.js v20 на Windows 7
Что делать если надо запустить современный софт в устаревшем окружении? Рассказ с картинками о процессе «портирования назад» последней версии Node.js на Windows 7.
Вводная
Как вы наверное и сами замечаете, прогресс в современном ИТ часто искусственен — не обоснован какими-либо техническими причинами, а обновления для массового ПО все чаще навязываются и делаются обязательными без желания пользователей.
Коммерческая поддержка и сопровождение устаревших версий при этом начинают стоить все более существенных денег.
Но в общем случае (в первую очередь речь про ваш домашний компьютер) действительно стоит обновляться и использовать последние версии всего ПО и операционных систем.
В первую очередь из-за патчей связанных с безопасностью.
Но есть места где это сделать не так просто: системы промышленной автоматики (АСУ), системы обслуживающие устаревший проприетарный софт, который просто не запускается на новых версиях ОC.
Как правило все что связано с промышленной автоматикой страдает от привязки к устаревшим версиям Windows, я сам неоднократно наблюдал как тяжело происходит процесс обновления у АСУшников с Windows 98/NT до XP/2003, с XP до Windows7 и так далее.
В общем это большое поле для деятельности, тут есть где развернуться.
Node.js и поддержка Windows 7
В качестве примера для раскрытия темы бекпортов я взял последнюю версию Node.js и портировал ее на Windows 7 SP1. И то и другое — 64битные.
Это достаточно небольшой (по сравнению например с .NET Core) и простой проект, позволяющий не утонуть в технических деталях и не раздуть статью до размеров книги.
Поддержка Windows 7 в Node.js закончилась еще в 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.
А последняя доступная для Windows 7 версия это 12.22.7, что разумеется не хватит для запуска и сборки свежих проектов на Node.js.
Если вы попробуете скачать и запустить свежую 20ю версию то увидите вот такое сообщение:
Официальная бинарная сборка Node.js v20 для Windows не заработает, послав вас в сторону сайта с обновлениями. Поэтому мы сделаем свою.
Немного технических деталей
Node.js — сам по себе достаточно старый и кроссплатформенный проект, причем Windows не является для него основной платформой.
С точки зрения задачи бекпортирования, это означает две вещи:
- места вызовов WinAPI изолированы и вынесены в отдельные файлы;
- ограничения на поддерживаемую ОС по большей части искусственны и большая часть кода проекта не имеет привязки к ОС и ее версии.
Но разумеется все не настолько просто, будут места где придется включать голову и немного думать.
Сам проект написан на C++ (и ожидаемо немного чистого Си), еще в нем активно используется Python для сборки — это первая серьезная проблема.
Дело в том что последние версии сборок Python для Windows также не поддерживают Windows7:
Note that Python 3.12.1 cannot be used on Windows 7 or earlier.
Но чтобы не заморачиваться сборкой еще и петона из исходников — я просто взял готовый сторонний бекпорт вот отсюда. Для сборки 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:
NASM
Внезапно в Node.js есть вставки на ассемблере, поэтому для сборки нужно поставить NASM. Я использовал последнюю версию 2.16.02rc7, с официального сайта.
Инсталлятор успешно отрабатывает на Windows 7, но к сожалению он не добавляет бинарники NASM в переменные окружения. Поэтому после установки необходимо вызвать sysdm.cpl и добавить папку с NASM в переменную PATH:
Visual Studio
Наконец последней, но самой жирной проблемой является сам компилятор C++. Вам нужно будет скачать и установить среду разработки Visual Studio 2019 — последнюю доступную версию для Windows 7, еще поддерживаемую скриптами сборки Node.js.
К сожалению Microsoft не любит когда используют устаревшие версии их софта, поэтому поиск ссылки на скачивание 2019й версии Visual Studio является еще тем квестом.
Я например нашел ее в этой статье, скачав версию «professional». Вот прямая ссылка для скачивания инсталлятора.
Стоит скачать и поставить именно среду разработки а не просто «build tools», поскольку нужно будет править код — делать это в голом «блокноте» не очень удобно.
Вот так выглядит у меня установленная версия:
Нужно будет установить вот эти пакеты целиком:
Установка займет ~22Гб места на диске и это лишь начало, имейте ввиду прежде чем в это лезть.
Сборка
Исходный код проекта Node.js можно забрать как из репозитория Github так и взять один из релизных архивов с исходниками.
Второй вариант быстрее, поскольку не надо выкачивать историю изменений и искать релизную ветку, поэтому я использовал его, скачав архив с исходниками с официального сайта.
Распаковываете архив например с помощью 7Zip в каталог без пробелов и символов юникода и запускаете сборку через скрипт vcbuild.bat в корне:
Разумеется сборка упадет, но не сразу: вы должны увидеть что нашлись и определились все необходимые инструменты для сборки — эта информация будет отображена в самом начале сборки. Убедившись что все нашлось, переходим к правке исходников.
Патчи
Первым делом отключаем очевидную заглушку, которая не дает использовать 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.
Для того чтобы заставить работать новый софт на старой ОС нужно либо эмулировать недоступную функцию либо использовать ее доступный аналог.
Второе сильно проще и поскольку проект 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 можно посмотреть вот тут.
Как видите вся переделка логики заключается в замене вызова и использовании немного других структур данных.
Последняя проблемная функция это 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:
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);
Сборка
Внесенных изменений будет достаточно для работы, поэтому после сохранения всех правок запускаем релизную сборку:
c:\work\node-v20.10.0>vcbuild.bat full-icu
Где аргумент full-icu указывает на поддержку всех локалей.
Сборка будет идти достаточно долго (в зависимости от оборудования) и если закончится успешно — в каталоге out\Release появится готовый билд.
Release\node.exe -e "console.log('Hello from Node.js', process.version)"
Должно произойти выполнение JavaScript кода и отобразиться версия только что собранного Node.js.
Но это еще не все, поскольку после сборки не будет пакетного менеджера NPM. Чтобы он появился, необходимо собрать финальный архив — такой же как вы скачивали с официального сайта.
c:\work\node-v20.10.0>vcbuild.bat package
После чего в папке out\Release появится еще один каталог с названием релиза:
Куда и будет добавлен скрипт для запуска npm.cmd.
Этот каталог — финальная собранная версия Node.js, которую можно использовать для сборки и запуска современных проектов.
Архив с дистрибутивом собирается с помощью 7zip, поэтому путь к нему должен быть в переменной PATH.
Пруфы работоспособности
Разумеется что самые упертые и подозрительные опытные из читателей, имеющие за плечами определенный опыт разработки и понимающие чем может грозить замена одного вызова WinAPI на другое усомнились будет ли такая «самопальная» версия вообще работать на реальных проектах.
Мне это тоже было интересно, поэтому я взял более-менее свежий и жирный boilerplate на Angular 16 в качестве тестового проекта, собрал и запустил.
Как видите запущен локальный сервер в режиме разработки с HMR (фоновой перекомпиляцией при изменениях) — самый сложный и ресурсоемкий вариант запуска.
Итого
Я хотел на каком-то простом и однозначном примере показать процесс бекпортирования — когда современный софт модифицируют для запуска на устаревшей ОС.
Разумеется что это очень простой случай, поскольку выбранный в качестве примера проект был изначально кроссплатформенным и содержал поддержку устаревшей ОС в прошлом — было сразу очевидно куда копать и что именно делать.
Тем не менее, судя по вою в интернете на тему поддержки Windows 7 в Node.js — даже такой «мини-бекпорт» оказывается много кому нужен.
Если же у вас есть подобные задачи по бекпортированию вашего собственного софта — пишите, поможем.