-
Notifications
You must be signed in to change notification settings - Fork 0
Demonstrating Native Addons #5
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
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-buildhas 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
nodeGypBuildends up calling therequire()function inside NodeJS. Which supports the ability to load*.nodebinaries (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
requireis supplied by the NodeJS runtime. If you execute the JS with a different runtime, they may support the commonjs standard, and thus understand therequirecalls, 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-buildwhich 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
requirecalls, bundlers will perform some sort of transclusion. This is also the case when ES6importtargets 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.cppand.has the most portable extensions.Additionally, there must be
binding.gypfile that looks like this:Basically another configuration file that configures
node-gypand how it should be compiling the C++ code. Thetarget_namespecifies the name of the addon file, so the output result will besomename.node. Thesourcesare self-explanatory. Theinclude_dirsentries have the ability to execute shell commands, in this case, it is usingnode -eto 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_twofunction that multiples a number by 2, and returns anint32number.It's also important that
node-gyp-buildis setup as ainstallscript 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-builddurin the installation process.This means that currently in our
utils.nixnode2nixDevexpression still requires thenpm installcommand. 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 installcommand is still run in order to execute build scripts. Andnode-gyp-buildis 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 rebuildcommand 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,makeandgcc. Ourshell.nixnaturally hasmakeandgccbecause we are usingpkgs.mkShellwhich must extend fromstdenv.mkDerivation. Howeverpython3will be needed as well.The
node2nixhas some understanding of native dependencies (this is why it also brings inpythonin 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
nodePackagesbecausenode2nixderivation isn't enough to build them (because they have complex native dependencies). Such asnode-gyp-builditself or vercel'spkg. This is also why I had to providenodePackages.node-gyp-buildin ourbuildInputsoverrides 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-dbends up forkingclassic-levelorleveldown. 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.srcfor--nodedirwhen it can just use thenodejssvanderburg/node2nix#295mkShellshould setNIX_NO_SELF_RPATH = true;by default NixOS/nixpkgs#173025Tasks
node-gyp-buildaddOnefor primitives andsetPropertyfor reference-passing procedure andmakeArrayfor heap allocationnixexpressions to supportnode-gyp-buildand other build scripts, and see if we can eliminate ourpostInstallhook, by relying onpackage.jsonhooks insteadprebuildifyto precompile binaries and host them on our git release... but this depends on whethertypescript-demo-libis used as a library or as an application, if used as an application, then thepkgbuilds 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.pkgintegration may just be a matter of setting theassetspath inpackage.jsonto the localprebuildsdirectory.[ ] 5. Cross compilation,- we must use CI/CD to do cross compilation (not sure about other architectures like ARM)prebuildify-crossor something else that uses Nix@typescript-eslintpackages 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_885403611pkgbundle 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 outputpkgso it's not storing random unnecessary things. This may mean thatpkgrequires dynamic--configto be generated.nix-build ./release.nix -A applicationcan 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 testcan 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)ldidorcodesignWIP: Demonstrating Native Addons TypeScript-Demo-Lib#38 (comment)pkgbundling script so that it doesn't bundle useless.mdfiles, right now it's even bundling theCHANGELOG.mdfiles WIP: Demonstrating Native Addons TypeScript-Demo-Lib#38 (comment)pkginstead ofziparchives 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:macosjob - 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