静态编译解决Ubuntu18.04下node20.17.0的Glibc版本问题

起因是我想要为俱乐部的官网项目仓库构建CI/CD工作流,以便于在每次有新的提交时自动执行构建检查并部署到服务器上,但是在实际操作中遇到了各种各样的问题,这里记录一下。

首先是,GitHub Actions 现在要求工作流的node环境版本会强制跑在 node20 版本之上,
本来这样没有什么问题,用nvm一套安装就行了,但是问题就在于我们服务器的 Ubuntu 系统版本是18.04,如果直接 use 20.17.0的话,动态库Glibc版本不匹配,因为18.04的Glibc版本是2.27,而20.17.0的Glibc版本要求是2.28,所以这个问题就很尴尬了。众所周知,Glibc是一个系统级别的库,大部分的Linux发行版直接升级 Glibc 都会导致系统不稳定,而另一方面,服务器刚刚重装过系统,如果硬要升级系统到20.04,又要考虑机器的性能,时间成本等问题,所以只能从 node 入手。

而一般来说,安装node有四种方式:

  • 最简单粗暴的就是通过包管理器直接安装:
    1
    sudo apt install nodejs
    但是由于多node版本的需求存在,我现在习惯于使用nvm来管理node版本:
    1
    2
    nvm install 20.17.0
    nvm use 20.17.0
    (nvm的安装也有几种方式,最简单的一种是通过curl直接安装,具体可以参考nvm的官方文档)

而上面两种问题都会因为 Glibc 的版本问题而导致即使下载了也不能用的情况。这个时候就要从动态链接库本身入手了。我们知道,动态链接和静态链接的区别在于,静态链接是将库文件的代码直接复制到可执行文件中,而动态链接则是在程序运行时才会去加载库文件。动态链接依赖于系统,这就是为什么ubuntu18.04的Glibc版本是2.27。但是,我们可以通过静态链接的方式来解决这个问题,因为静态链接是系统无关的。我们所需要做的就是自己拿到源代码,手动配置完全静态编译,这样就不会依赖于系统的动态链接库版本了。

所以剩下的两种 node 安装方式就是:

  • 预编译的二进制文件
  • 源代码

但是官网上前者也是依赖于系统的动态链接库的,所以只能选择后者了。这里我选择了源代码的方式,因为这样可以自己配置编译参数,而且可以保证是完全静态编译的。
https://stackoverflow.org.cn/questions/17943595
首先,我们需要下载 node 的源代码,这里我选择了20.17.0版本:

1
2
3
4
# 如果因为网络原因无法下载,也可以本地下载后上传到服务器上解压
wget https://nodejs.org/dist/v20.17.0/node-v20.17.0.tar.gz
tar -zxvf node-v20.17.0.tar.gz
cd node-v20.17.0

然后,我们需要配置编译参数,这里我选择了完全静态编译:
1
./configure --prefix=/usr/local/node-v20.17.0 --fully-static

最后,我们需要编译并安装:
1
2
3
4
# 这里的-j4是指并行编译,可以根据自己的机器性能调整
make -j4
# install是指安装到系统目录,那么编译出来的node就在当前目录下
sudo make install

需要注意的是,编译所需要的内存和硬盘空间都是比较大的,所以在编译的时候要确保机器有足够的资源。如果服务器的内存不够,可以在本地内存开的更多的Linux机器上编译完再 scp 到服务器上。

这样,我们就可以在服务器上安装 node 20.17.0 版本了,而且是完全静态编译的,不会受到系统动态链接库的影响。

检查是不是完全静态编译的方法是:

1
ldd /usr/local/node-v20.17.0/bin/node

如果输出是 not a dynamic executable,那么就是完全静态编译的。

然后需要在.bashrc文件中配置环境变量:

1
2
echo 'export PATH=/usr/local/node-v20.17.0/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

检查是否安装成功:
1
2
node -v
npm -v