-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Demonstrating Native Addons #2
Conversation
…ot cause a full rebuild of the derivatons
👆 Click on the image for a new way to code review
Legend |
The That should create a git tag. Then when we push this git tag, a job called Technically this was meant to be like this, however, you can submit tag pointing to any commit, it doesn't have to be pointing to a staging branch. Ideally, upon a commit coming into staging branch, and if that commit had a tag pre-release associated, then we would perform the However there's no actual good rules for triggering something like this. What we could do is run a job that checks whether to do a prerelease by checking if the commit also has a tag. It seems like this should be possible, as long as the git repo also has the tag information when cloned in. The other alternative is to run the job any time a prerelease tag occurs. This is basically how I started, but the prerelease job has |
One of the problems with relying on commits is that pipelines are interruptible. So right now, if a commit that is meant to mean some sort of release is pushed, that may be hidden in other commits that interrupt it. Currently Gitlab CI doesn't have a way of having conditional interruption: https://gitlab.com/gitlab-org/gitlab/-/issues/194023 This means we have to use tags. However the previous reason why tagging wasn't working before was because I had it limited to protected refs, and I had to temporarily disable protection on these refs in order to test out these different ways of doing things. |
The only downside to having tag pipelines is that it doubles up the pipeline when a commit is submitted too. That is since So have a tradeoff here. Either we allow a pipeline to run twice which is a bit of a waste, or we allow for the possibility of skipped prerelease commits. It isn't just when another commit is pushed, but one may perform |
I believe for the second condition we will need to only perform the task on the tag pipeline. As for the first problem, we would need to add something that checks if the commit is associated to a release commit, and if so, skip it. |
This was accidentally closed due to merge into staging. Need to reopen. |
Description
In helping solve the snapshot isolation problem in MatrixAI/js-db#18, we needed to lift the hood and go into the C++ level of nodejs.
To do this, I need to have a demonstration of how native addons can be done in our demo lib here.
There are 2 ecosystems for building native addons:
Of the 2, the prebuild ecosystem is used by UTP and leveldb. So we will continue using that. Advantages from 2016 was commented here: prebuild/prebuild#159
The basic idea is that Node supports a "NAPI" system that enables node applications to call into C++. So it's a the FFI system of NodeJS. It's also a bidirectional FFI as C++ code can call back into the NodeJS JS functions.
The core library is
node-gyp
. In the prebuild ecosystem is wrapped withnode-gyp-build
, which you'll notice is the one that we already using in this repo. The main feature here is the ability to supply prebuilt binaries instead of expecting the end-user to always compile from source.Further details here: https://nodejs.github.io/node-addon-examples/build-tools/prebuild (it also compares it to node-pre-gyp).
The
node-gyp-build
has to be adependency
, notdevDependencies
, because it is used during runtime to automatically find the built shared-object/dynamic library and to load it.It looks like this:
Internally
nodeGypBuild
ends up calling therequire()
function inside NodeJS. Which supports the ability to load*.node
binaries (which is the shared-object that is compiled using the NAPI C++ headers). See: https://github.com/prebuild/node-gyp-build/blob/2e982977240368f8baed3975a0f3b048999af40e/index.js#L6The
require
is supplied by the NodeJS runtime. If you execute the JS with a different runtime, they may support the commonjs standard, and thus understand therequire
calls, but they may be compatible with native modules that are compiled with NAPI headers. This is relevant since, you also have to load the binary that matches your OS libraries and CPU architecture. It's all dynamic linking under the hood. This is also why you usenode-gyp-build
which automates some of this lookup procedure.As a side-note about bundlers. Bundlers are often used part of the build process that targets web-platforms. Since the web platform does not understand
require
calls, bundlers will perform some sort of transclusion. This is also the case when ES6import
targets files on disk. Details on this process is here: https://github.com/evanw/esbuild/blob/master/docs/architecture.md#notes-about-linking. Bundlers will often call this "linking", and when targetting web-platforms, this is basically a form of static linking since JS running in browsers cannot load JS files from disk. This is also why in some cases, one should replace native addons with WASM instead, as bundlers can support static linking of WASM (which are cross-platform) into a web-bundle. But some native addons depend on OS features (like databases with persistence), and fundamentally cannot be converted into WASM binaries. In the future, our crypto code would make sense to turn into WASM binaries. But DB code is likely to always be native, as they have to be persistent. As the web develops can gains extra features, then eventually it may be possible that all native code can be done via WASM (but this may be a few years off).Now the native module itself is just done with a C++ file like
index.cpp
. We should prefer using.cpp
and.h
as the most portable extensions.Additionally, there must be
binding.gyp
file that looks like this:Basically another configuration file that configures
node-gyp
and how it should be compiling the C++ code. Thetarget_name
specifies the name of the addon file, so the output result will besomename.node
. Thesources
are self-explanatory. Theinclude_dirs
entries have the ability to execute shell commands, in this case, it is usingnode -e
to execute a script that will return some string that is a path to C++ headers that will be included during compilation.The C++ code needs to use the NAPI headers, however there's a macro library that makes writing NAPI addons easier: https://github.com/hyperdivision/napi-macros. I've seen this used in the utp-native and classic-level.
The C++ code may look like this:
This ends up exporting a native module containing the
times_two
function that multiples a number by 2, and returns anint32
number.It's also important that
node-gyp-build
is setup as ainstall
script in thepackage.json
:This means when you run
npm install
(which is used to install all the dependencies for a NPM package, or to install a specific NPM package), it will run thenode-gyp-build
durin the installation process.This means that currently in our
utils.nix
node2nixDev
expression still requires thenpm install
command. This used to exist, however I removed it during MatrixAI/TypeScript-Demo-Lib#37 thinking it had no effect. But it was confirmed by svanderburg/node2nix#293 (comment) that thenpm install
command is still run in order to execute build scripts. Andnode-gyp-build
is now part of the installation process. We should include: https://github.com/svanderburg/node2nix/blob/8264147f506dd2964f7ae615dea65bd13c73c0d0/nix/node-env.nix#L380-L387 with all the necessary flags and parameters too. We may be able to make it work if we hook our build command prior tonpm install
. I imagine that this should be possible since thenpm rebuild
command is executed prior. So we need to investigate this.In order to make this all work, our Nix environment is going to need all the tools for source compilation. Now according to https://github.com/nodejs/node-gyp#on-unix we will need
python3
,make
andgcc
. Ourshell.nix
naturally hasmake
andgcc
because we are usingpkgs.mkShell
which must extend fromstdenv.mkDerivation
. Howeverpython3
will be needed as well.The
node2nix
has some understanding of native dependencies (this is why it also brings inpython
in its generated derivation svanderburg/node2nix#281), and I believe it doesn't actually build from source (except in some overridden dependencies).Some npm dependencies are brought in via nixpkgs
nodePackages
becausenode2nix
derivation isn't enough to build them (because they have complex native dependencies). Such asnode-gyp-build
itself or vercel'spkg
. This is also why I had to providenodePackages.node-gyp-build
in ourbuildInputs
overrides inutils.nix
. It is important that any dependencies acquired via nixpkgs must be the same version we use in ourpackage.json
. And this is the case for:Ideally we won't need to do this our own native packages if
js-db
ends up forkingclassic-level
orleveldown
. I think this trick is only relevant in our "build tools" and not our runtime dependencies.The remaining problem is cross-compilation, as this only enables building from source if you are on NixOS and/or using Nix. Windows and MacOS will require their own setup. Since our development environment is all Nix focused, we don't have to worry about those, but for end-users who may want to rebuild from scratch, they will need to setup their development environent based on information in https://github.com/nodejs/node-gyp. A more pressing question is how we in our Nix development environment will be capable of cross-platform native addons for distribution.
This is where the prebuild ecosystem comes in and in particular https://github.com/prebuild/prebuildify-cross. This is used in leveldb to enable them to build for different platforms, and then save these cross-compiled objects. These objects are then hosted on GitHub releases, and automatically downloaded upon installation for downstream users. In the case they are not downloadable, they are then built from source. https://github.com/Level/classic-level/blob/f4cabe9e6532a876f6b6c2412a94e8c10dc5641a/package.json#L21-L26
However in our Nix based environment, I wonder if we can avoid using docker to do cross compilation, and instead use Nix to provide all the tooling to do cross-compilation. We'll see how this plays out eventually.
Some additional convenience commands now:
Issues Fixed
nodejs.src
for--nodedir
when it can just use thenodejs
svanderburg/node2nix#295mkShell
should setNIX_NO_SELF_RPATH = true;
by default NixOS/nixpkgs#173025Tasks
node-gyp-build
addOne
for primitives andsetProperty
for reference-passing procedure andmakeArray
for heap allocationnix
expressions to supportnode-gyp-build
and other build scripts, and see if we can eliminate ourpostInstall
hook, by relying onpackage.json
hooks insteadprebuildify
to precompile binaries and host them on our git release... but this depends on whethertypescript-demo-lib
is used as a library or as an application, if used as an application, then thepkg
builds is used, if used as a library, then one must install the native binary from the same github release, this means the native binary must be part of the same release page.pkg
integration may just be a matter of setting theassets
path inpackage.json
to the localprebuilds
directory.[ ] 5. Cross compilation,- we must use CI/CD to do cross compilation (not sure about other architectures like ARM)prebuildify-cross
or something else that uses Nix@typescript-eslint
packages to match js-db to avoid the warning message.[ ] 8. Update README.md to indicate the 2 branches of typescript-demo-lib, the main and the native branch, where the native branch indicates how to build native addons- this will be done in a separate repo: https://github.com/MatrixAI/TypeScript-Demo-Lib-Native based off https://gitlab.com/MatrixAI/Employees/matrix-team/-/issues/8#note_885403611pkg
bundle can receive optimisation on which prebuild architectures it bundles, right now it bundles all architectures, when the target architecture implies only a single architecture is required. This can slim the final outputpkg
so it's not storing random unnecessary things. This may mean thatpkg
requires dynamic--config
to be generated.nix-build ./release.nix -A application
can be useprebuilds/
directory as well, as this can unify withpkg
. That way all things can useprebuilds/
directory. But we would want to optimise it with task 10.[ ] 12. Ensure that- this can be done in polykey as a scriptnpm test
can automatically run general tests, and platform-specific tests if detected on the relevant platformFuture Tasks
win-arm64
,linux-arm64
(linux will require the necessary nix-shell environment)ldid
orcodesign
WIP: Demonstrating Native Addons TypeScript-Demo-Lib#38 (comment)pkg
bundling script so that it doesn't bundle useless.md
files, right now it's even bundling theCHANGELOG.md
files WIP: Demonstrating Native Addons TypeScript-Demo-Lib#38 (comment)pkg
instead ofzip
archives so you can do stapling and therefore not require the client systems to have access to the internet before running the executable: WIP: Demonstrating Native Addons TypeScript-Demo-Lib#38 (comment)integration:macos
job - WIP: Demonstrating Native Addons TypeScript-Demo-Lib#38 (comment)npm test
(it should automatically understand how to conditionally test these things by loading files appropriately in the right platform, or just a script that knows): https://stackoverflow.com/questions/50171932/run-jest-test-suites-in-groupsFinal checklist