PKG Build Process
pkg has so called “base binaries” - they are actually same node executables but with some patches applied. They are used as a base for every executable pkg creates. pkg downloads precompiled base binaries before packaging your application.
When package a project, pkg will try to get the node binaries from below locations:
- PKG local cache folder, the default location is
%userprofile%/.pkg-cache
, can usePKG_CACHE_PATH
environment variable to specify a custom path - If not found the local cache, pkg-fetch will try to retrieve from remote path
- If not found in remote, will download source code from node.js official and compile it. The compile result will be saved to pkg local cache folder.
Due to limited resource, PKG now only supports node 64 bit. When package a 32 bit node project, we need to compile 32bit node and patch it by ourselves. Below sections demo the whole process.
Compile Node and Node Patch
VisualStudio 2022 does not support 32 bit OS anymore, so we need to setup node build environment in 64 bit OS, both client and server OS should be fine.
All below sub sections are done on Windows Server 2022 64 bit machine. This is also the official Node binary build platform mentioned in official-binary-platforms
Setup Node build Environment
Follow node official guide BUILDING.md to setup build environment.
Be attention the official guide may change based on the node tag version you select.
Create Nodejs Patch(Optional)
Note 2024-11-04:
@yao-pkg/pkg-fetch already provided the patch file of node v22.11.0. The patch files are available at patches. So we can use this official patch directly.
This section is preserved and should only be used when there is no official patch available.
pkg-fetch has a guide on how to create a node patch.
-
Clone Node.js as a sibling to your current pkg-fetch clone
1 2 3 4 5
mkdir C:\git cd c:\git git clone https://github.com/yao-pkg/pkg-fetch.git // clone pkg-fetch git clone https://github.com/nodejs/node.git // clone node cd node
-
Checkout the tag you wish to generate a patch for.
1
git checkout v22.11.0
-
Attempt to apply the closest patch (e.g. applying the existing patch for 18.15.0 when trying to generate a new patch for 18.14.0)
1
git apply ..\pkg-fetch\patches\node.v22.11.0.cpp.patch --reject
-
If no rejects, great! you are ready to make your new patch file
1 2
git add -A git diff --staged --src-prefix=node/ --dst-prefix=node/ > ..\pkg-fetch\patches\node.v22.11.0_new.cpp.patch
-
If rejects exist, search all
*.rej
, and resolve them yourself, and ensure all changes are saved, and repeat step 4 to export the patch file
Attention
- Make sure the encoding of generated patch file is
UTF-8
. - Uses the
git apply --check node.v18.14.0.cpp.patch
on a clean node v18.14.0 branch to ensure the patch file is valid.
How to resolve the rejection
All need to do is to manually merge the change from src/xxx.cc.rej
into src/xxx.cc
and remove the *.rej
file.
After resolve all files, please repeat step 4 to export the patch file.
Build Node with patch
-
Download node v20.18.0 x64 binaries from https://nodejs.org/dist/v20.18.0/, unzip to
C:\git\node-v20.18.0-win-x64
. Set windows environment, add node location to global path, so we can recognize node and npm in cmd.Note: The version
20.18.0
looks weird but quite important to the nodejs building in the following steps. When I used node v22.11.0, there are lots of unknown errors during compiling. I saw @yao-pkg/pkg-fetch use node v20 to setup environment(see Dockerfile.linux), that maybe the reason.
I got this tip from here -
clone pkg-fetch:
1
git clone https://github.com/yao-pkg/pkg-fetch.git
-
Goto pkg-fetch folder, install yarn globally.
1
npm install -g yarn
-
Install prerequisite node_modules
1
yarn install --ignore-engines
-
Goto patches folder, make sure you have got the correct patch file for the node version you want to build. And confirm the config in
patches.json
is correct. Config file should record the correct node version and patch file. -
Run below command to compile node. It will take a long time to complete.
1
yarn start --node-range node22 --arch x86 --output dist
Note: When specify node-range as node22, pkg-fetch will first search under patches folder to find node 22 patch, then use this node version to download and compile the node source code. So the node version in
patches.json
is very very important. - After compile complete, the node v22 is ready at dist folder, filename like this
node-v22.11.0-win-x86
. Rename it tobuilt-v22.11.0-win-x86
andbuilt-v22.11.0-win-x86.sha256sum
- copy files
built-v22.11.0-win-x86
andbuilt-v22.11.0-win-x86.sha256sum
toc:\git\output\pkg\.pkg-cache\v3.5
- Copy
c:\git\output
to a file server for later use.
That’s all we need to do on node build machine
Download pkg source code
On our develop machine
There is an example project under pkg source code, which is written using express. We would use this project to demo how to use pkg to package node 32 bit project files to a single executable application. We can also use this project to verify if our final build environment is correct.
1
2
3
mkdir C:\git\pkg\demo
cd C:\git\pkg\demo
git clone https://github.com/yao-pkg/pkg.git
the example project is under C:\git\pkg\demo\pkg\examples\express
Add pkg as dev dependency:
1
2
3
cd C:\git\pkg\demo\pkg\examples\express
npm install @yao-pkg/pkg --save-dev
node index.js
Open web browser, input url http://localhost:8080/
, should show Hello, world!
Package our demo project
- Copy the output folder generated on 32 bit machine to develop machine
C:\git\pkg\demo\output
-
Set PKG cache path
1 2
cd C:\git\pkg\demo\pkg\examples\express set PKG_CACHE_PATH=C:\git\pkg\demo\output\pkg\.pkg-cache
- (optional, required if we have self-built patch) Copy pkg-fetch patch file from
C:\git\demo\output\pkg-fetch\patches
toC:\git\demo\pkg\examples\express\node_modules\pkg-fetch\patches
-
Run below command to generate exe
1 2
cd C:\git\pkg\demo\pkg\examples\express node .\node_modules\@yao-pkg\pkg\lib-es5\bin.js . -t node22-win-x86 -o .\output\test.exe --debug > debug.log
PKG should not download and compile Node anymore. There should be an exe generated under
C:\git\pkg\demo\pkg\examples\express\output
Check debug.log, the log should end like below1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
> [debug] Targets: [ { "nodeRange": "node22", "platform": "win", "arch": "x86", "output": "xxx", "forceBuild": false, "fabricator": { "nodeRange": "node22", "platform": "win", "arch": "x86", "binaryPath": "xxx" }, "binaryPath": "xxxx" } ]
If the log end with any error or fail, should check and fix it.
- Double click to run test.exe
Open web browser, input urlhttp://localhost:8080/
, should showHello, world!
Notes
Location
npm global install location:
If you installed node or npm by installer, the npm module global install path should be %userProfile%\AppData\Roaming\npm\node_modules
.
If you download node binaries and set node path in system environment, the npm module global path should be <your node path>\node_modules
Pkg debug
Add debug information during pkg build package
1
node .\node_modules\pkg\lib-es5\bin.js . -t node18-win-x86 -o .\output\test.exe --debug
Output the file list
1
2
SET DEBUG_PKG=1
test.exe > fileList.log
Missing SafeSEH
There is a DFS issue found, the exe we built is missing SafeSEH.
This issue only occurs in x86 exe, 64 bit exe does not have this flag.
When do packaging, PKG appends our source files at the end of node.exe, then unzip it when execute. So what we need to do is to add SafeSEH flag for node.exe.
As pkg does not provide 32 bit node, we have to build our own 32 bit node. Detail please refer section Build Node with patch.
The workflow is:
- Pkg-fetch check if there is available node22 source code. If no, it will download from node official. Cache path is:
%userprofile%\.pkg-cache\node
- After download complete, unzip it to temp folder
%userprofile%\AppData\Local\Temp\pkg.xxxxxx\node
- Call node commands to compile node projects
i. Use .gyp file to generate vc project and solution files
ii.Use MSBuild to compile node projects - After compile complete, zip node.exe, output it to
pkg-fetch\dist
What we need to do is to modify the vcxproj file generated in step 3.i. Add /SafeSeh
flag for node project.
Modify vcxproj
Note: Below is not the final solution steps. I add this section to record my investigate process. It will be useful if we upgrade node to a higher version in the future.
-
clone node source code
1
git clone https://github.com/nodejs/node.git
-
Checkout the tag .
1
git checkout v22.11.0
-
Run cmd with below command to generate project files only
1
vcbuild.bat x86 projgen nobuild
-
Open node.sln with VS, open the property setting of node project, in Linker - Advanced, change
Image Has Safe Exception Handlers
asYes (/SAFESEH)
.
-
Build node project, there are errors popup
1 2 3
60>v8_base_without_compiler.lib(push_registers_masm.obj) : error LNK2026: module unsafe for SAFESEH image. 60>v8_snapshot.lib(embedded.obj) : error LNK2026: module unsafe for SAFESEH image. 60>out\Debug\node.exe : fatal error LNK1281: Unable to generate SAFESEH image.
-
There are two libraries are unsafe for SAFESEH image:
v8_base_without_compiler
andv8_snapshot
.
Takev8_base_without_compiler
as example, this project includes a MASM file which is unsafe.
To fix this issue, in node solution, goto(tools) - (v8_gypfiles)
, findv8_base_without_compiler
project, right click to open property setting, in ` Microsoft Macro Assembler - Advanced, change
Use Safe Exception Handlersas
Yes (/safeseh)`.
Same change forv8_snapshot
project. -
After change these setting, recompile node project. If compile succeed, we are ready to goto next section.
Modify gyp file
Mentioned in section3.i, the node vcxproj files are auto generated. Node uses gyp to generate these project files. What we need to do is to modify the gyp file, add additional msvc setting, so the vcxproj generated will contain the safeseh
setting we want.
-
In node root folder, find node.gyp, search
ImageHasSafeExceptionHandlers
, you should find only one result:1 2 3 4 5 6 7 8
# Relevant only for x86. # Refs: https://github.com/nodejs/node/pull/25852 # Refs: https://docs.microsoft.com/en-us/cpp/build/reference/safeseh-image-has-safe-exception-handlers 'msvs_settings': { 'VCLinkerTool': { 'ImageHasSafeExceptionHandlers': 'false', }, },
change default
false
totrue
. There are two links in comments you can refer. -
In tools\v8_gypfiles folder, find v8.gyp. Use
'target_name': 'v8_base_without_compiler'
as key word to findv8_base_without_compiler
, add msvc setting under it. This project doesn’t have default SafeImage setting.1 2 3 4 5
'msvs_settings': { 'MASM': { 'UseSafeExceptionHandlers': 'true', }, },
Same change for
v8_snapshot
. It is in v8.gyp too. -
After modifying gyp file, run command to regenerate vcxproject file, make sure they are correct.
1
vcbuild.bat x86 projgen nobuild
Generate patch file
Above two sections happens in node repo, what we need to do now is to reproduce our changes in pkg-fetch. pkg-fetch will auto download node source and compile it. To insert our changes, we need to generate a new patch for node and merge our patch into pkg-fetch’s.
-
In node repo root folder, run below command:
1 2
git add -A git diff --staged --src-prefix=node/ --dst-prefix=node/ > ..\node.v22.11.0_safeImage.cpp.patch
-
Open patch file, make sure only
node.gyp
andv8.gyp
are changed. Copy all contents of our patch. -
Goto pkg-fetch\patches, find node.v22.11.0.cpp.patch, paste our patch at the end.
-
Follow section Build Node with patch step 6, run command
1
yarn start --node-range node22 --arch x86 --output dist
You should be able to get a x86 node with safeSEH enabled.
Check SafeSEH flag
We can use a powershell script to check if safeSEH is enabled.
-
clone repo PESecurity:
1
git clone https://github.com/NetSPI/PESecurity
-
open a powershell window and import this module:
1 2
cd C:\git\PESecurity Import-Module .\Get-PESecurity.psm1
-
Check file PE property:
1 2
Get-PESecurity -file "C:\git\pkg-fetch\dist\node-v22.11.0-win-x86" Get-PESecurity -directory C:\Windows\System32\