-
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 #5
Conversation
👆 Click on the image for a new way to code review
Legend |
daf8abb
to
9af53a1
Compare
Now it's back to tag pipeline, but we put in the necessary checks to prevent running when the commit title is a version release tag. |
9af53a1
to
5249a05
Compare
The |
Now that prerelease build is working fine. Final step is release job which is done after integration. We will use our original job which produces a GH release and tag. But also combine it with a release job In fact our |
Also we'd like to auto-merge the staging into master when this all passes. This should be done after all the integration runs are done. This will require the |
Actually automerge via So another way is just to use There's also push options that enable auto-creating a MR on gitlab... or using So keep it simple and just merge into master, and push it up. That will trigger the pipeline on master branch. Like a recursive pipeline. What are the permissions needed to write back to the repo. |
For the push back to origin, we may need to authenticate either via SSH or HTTP. I believe HTTP would be the best. Some sort of token needs to be available for the projects that authenticate themselves to the same project. |
I found https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html but it's a bit more complicated than that. Firstly I'm not sure if it can even push up to the origin. Alternatively access token exists on each project, but that has to be setup individually for each project. Finally all of this would only affect the gitlab repository which is a pull mirror of github. So the actual auth needed is github. Which is why we wanted to use |
I think we might need to use We now have a nice way of merging staging to master. Note that further jobs run after merging to master. In this case production deployment, production deployment tests, then final production release. |
Some final issues:
|
Changing to typescript-demo-lib-native
7fa2585
to
516ff81
Compare
To test merging from feature-native to staging. I need to use Generally speaking we would want feature branches to fast forward into staging branch, and this is enforced by GitHub. According to https://stackoverflow.com/questions/60597400/how-to-do-a-fast-forward-merge-on-github, GitHub has no way of doing fast forward merges on PRs. This means:
The original plan was to have linear commits between master and staging, but this is also acceptable for now. |
4b8393a
to
ed184b2
Compare
Description
Derived from #2
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