kriszyp / lmdb-js

Simple, efficient, ultra-fast, scalable data store wrapper for LMDB
Other
520 stars 44 forks source link

No native build was found for platform=darwin arch=arm64 runtime=electron abi=114 uv=1 armv=8 libc=glibc node=18.14.0 electron=24.8.7 webpack=true #279

Open thomasdao opened 8 months ago

thomasdao commented 8 months ago

Hi,

I have been able to integrate lmdb into our Electron app and it is running really well. When I package the app, it can run normally on my Macbook Pro M1 and Windows 11 computer, however when the app runs on another Macbook Air M1 (Ventura 13.4.1), I saw the error below

Uncaught Error: No native build was found for platform=darwin arch=arm64 runtime=electron abi=114 uv=1 armv=8 libc=glibc node=18.14.0 electron=24.8.7 webpack=true
    attempted loading from: /Users/username/project/node_modules/lmdb and package: @/-darwin-arm64

I was not sure what could cause this issue. Including or excluding the node_modules folder into the packaged app still shows the error. I go to the node_modules/lmdb and run npm run build, and error still happens.

I'd really appreciate if you have any idea what could cause this issue, many thanks!

kriszyp commented 8 months ago

I go to the node_modules/lmdb and run npm run build, and error still happens.

Are you saying that node can't load lmdb either? (like if you just require('.') in the lmdb directory).

runtime=electron abi=114

This is a weird abi, the abi for Node 18.14.0 is 108 (you can get that from process.versions.modules), and for Node 20 it is 115. I have never heard of abi=114, but maybe that is an electron version.

/Users/username/project/node_modules/lmdb and package: @/-darwin-arm64

This should be @lmdb-darwin-arm64, it looks like it is somehow failing to retrieve the package name from the lmdb package here: https://github.com/kriszyp/node-gyp-build/blob/ngbop/index.js#L38

thomasdao commented 8 months ago

Thanks for your reply. I suspect that maybe the app is trying to access the local file at /Users/username/Documents/project/node_modules/lmdb.

When I package the app in my Macbook Pro and open the app, I saw a dialog asking for permission to access the Documents folder, something not happened before.

I transfer the app to my Macbook Air and see the error above. It's probably because the local path is different, the app cannot load the native module.

I then build the app in my Macbook Air and now when I open the app, I saw the same dialog asking for permission to access Documents folder, and after that the app can runs normally.

I tested release to TestFlight, and the app from TestFlight has the same error.

kriszyp commented 8 months ago

the app is trying to access the local file at /Users/username/Documents/project/node_modules/lmdb.

Yes, it looks at the path to see if there is a build in there, and then tries to read the package.json to determine the package name, to determine the native build package (should be @lmdb-darwin-arm64 in this case).

When I package the app in my Macbook Pro and open the app, I saw a dialog asking for permission to access the Documents folder, something not happened before.

Is this possibly a permission issue, that it can't access these files?

thomasdao commented 8 months ago

I did a bit more debugging, and I believe the issue is at this function:

function resolveFile (prebuilds) {
    // Find most specific flavor first
    console.log('resolveFile', prebuilds)
    var parsed = readdirSync(prebuilds).map(parseTags)
    var candidates = parsed.filter(matchTags(runtime, abi))
    var winner = candidates.sort(compareTags(runtime))[0]
    if (winner) return path.join(prebuilds, winner.file)
}

In both of my Macbook Pro and Macbook Air, the app is accessing the folder at the absolute path /Users/username/Documents/project/node_modules/@lmdb/lmdb-darwin-arm64, that's probably why the MacOS trigger the permission dialog for the readdirSync function. However this absolute path is only valid in my Macbook Pro and not in my Macbook Air.

I think if readdirSync use the relative path to the app's root folder, the app should be able to locate and load the native module at @lmdb/lmdb-darwin-arm64. I'll try a bit more debugging if it can work.

kriszyp commented 8 months ago

Interesting, I wonder why the path would not be valid on your Macbook Air since it is coming from relative path to lmdb: https://github.com/kriszyp/lmdb-js/blob/master/native.js#L6

thomasdao commented 8 months ago

I log import.meta.url on both Macbook Pro and Macbook Air and this variable is the same absolute path. Is it possible that Webpack translates this variable to an absolute string? I'm using latest Webpack version 5.90.3.

thomasdao commented 8 months ago

I saw this issue https://github.com/webpack/webpack/issues/14445 which states that by default import.meta.url is replaced by the current file location at the build time.. This can be disabled in Webpack by adding below setting:

module.exports = {
  module: {
    parser: {
      javascript: {
        importMeta:false
      },
    },
  },
};

However when I added that setting into the webpack.config.js, I have a new error SyntaxError: Cannot use 'import.meta' outside a module.

thomasdao commented 8 months ago

@kriszyp I managed to patch lmdb/native.js and node-gyp-build-optional-packages/index.js file to point directly to the @lmdb folder inside the Electron asar file, and now the app can run in both of my Macbook Pro and Macbook Air.

My next obstacle is to support both ARM and Intel for Mac. Some libraries like classic-level provides fat binary which support both arm64 and x64, but in @lmdb I can only see lmdb-darwin-arm64 folder. Do you know how to build the fat binary for Mac? Thank you.

kriszyp commented 8 months ago

Do you know how to build the fat binary for Mac?

I think https://github.com/prebuild/prebuildify has some information on how to reference a universal binary, but I don't know how to build one. And that wouldn't help at all for Windows and Linux would it? I have never really been interested in supporting only a single platform, lmdb-js is intended to support all platforms.

thomasdao commented 8 months ago

For Windows and Linux, the current approach works fine, however for Mac, Electron app needs to be built in universal format to support both Intel and ARM architecture when release to the Mac App Store. That's why some libraries bundle both arm and x64 in one fat binary (only for Mac, nothing changes for other platforms).

One idea I can think of is that I can force download the lmdb-darwin-x64 and manually add to Electron app on build step for Mac, but I'm not sure if that could work. Currently I can't manually install @lmdb/lmdb-darwin-x64 from npm because the architecture (x64) is not on my Macbook Pro (ARM).

kriszyp commented 8 months ago

I don't know if it helps at all, but lmdb-js does come with a bin script download-lmdb-prebuils to download the prebuilt binaries for all platforms, so they are all in place (we use it to create offline install packages).

thomasdao commented 8 months ago

I tried node ./bin/download-prebuilds.js but have this error /bin/sh: download-msgpackr-prebuilds: command not found. I don't know how to setup download-msgpackr-prebuilds command.

I go to the node_modules/lmdb and run yarn recompile to compile from source instead. I added below settings to the binding.gyp to produce universal binary for Mac, borrowed from classic-level:

        ["OS == 'mac'", {
          "cflags+": ["-fvisibility=hidden"],
          "xcode_settings": {
            # -fvisibility=hidden
            "GCC_SYMBOLS_PRIVATE_EXTERN": "YES",

            # Set minimum target version because we're building on newer
            # Same as https://github.com/nodejs/node/blob/v10.0.0/common.gypi#L416
            "MACOSX_DEPLOYMENT_TARGET": "10.7",

            # Build universal binary to support M1 (Apple silicon)
            "OTHER_CFLAGS": [
              "-arch x86_64",
              "-arch arm64"
            ],
            "OTHER_LDFLAGS": [
              "-arch x86_64",
              "-arch arm64"
            ]
          }
        }]

I'm not sure if I need to recompile @msgpackr-extract to reproduce universal binary for Mac. When I run yarn recompile to build from source, does the final binary also include msgpackr-extract? Thank you.

kriszyp commented 8 months ago

I'm not sure if I need to recompile @msgpackr-extract to reproduce universal binary for Mac.

The binary doesn't include msgpackr-extract, but msgpackr-extract is just an optional dependency, so it is not needed (just has performance boost for deserialization, but everything should run fine without it).

thomasdao commented 8 months ago

Thanks for the clarification. I found a strange issue - when I run yarn recompile to produce binary in build/Release or prebuildify to produce binary in prebuilds folder, both with or without the change to produce the universal binary, the app crashes with below error:

  [13583:0319/215749.375003:ERROR:node_bindings.cc(156)] Fatal error in V8: v8_ArrayBuffer_NewBackingStore When the V8 Sandbox is enabled, ArrayBuffer backing stores must be allocated inside the sandbox address space. Please use an appropriate ArrayBuffer::Allocator to allocate these buffers, or disable the sandbox.

I search on Google and found this error is due to Electron memory cage: https://www.electronjs.org/blog/v8-memory-cage

However when I delete the build folder to let lmdb selects the prebuild in @lmdb folder, the app runs normally. Maybe there's some different with the build from source and prebuilt binary?

kriszyp commented 8 months ago

I believe the difference may be that the NAPI build does not use the backing store memory allocation, so you may want to use that build.

thomasdao commented 8 months ago

Update: I was able to build the universal app for both arm64 and x64 architecture! I've tested in both M1 and Intel computer and they work great!

Then I've code-signed and released the app to TestFlight without any warning. However after the app is downloaded from TestFlight, it crashes with below error. I believe it is due to Apple sandbox in Mac. I've opened a support ticket with Apple and hope to get their reply soon, but if you have any idea it would be great. Thank you.

Thread 0 Crashed:: CrRendererMain Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib                 0x184284724 __pthread_kill + 8
1   libsystem_pthread.dylib                0x1842bbc28 pthread_kill + 288
2   libsystem_c.dylib                      0x1841c9ae8 abort + 180
3   libsystem_malloc.dylib                 0x1840eae28 malloc_vreport + 908
4   libsystem_malloc.dylib                 0x1840ee6e8 malloc_report + 64
5   libsystem_malloc.dylib                 0x1840faf20 find_zone_and_free + 308
6   lmdb.napi.node                         0x1050f1980 EnvWrap::closeEnv(bool) + 832
7   lmdb.napi.node                         0x1050f2dd0 EnvWrap::openEnv(int, int, char const*, char*, Compression*, int, int, unsigned long, int, unsigned int, unsigned int, char*) + 700
8   lmdb.napi.node                         0x1050f25f0 EnvWrap::open(Napi::CallbackInfo const&) + 1568
9   lmdb.napi.node                         0x105109548 Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()::operator()() const + 252
10  lmdb.napi.node                         0x10510942c napi_value__* Napi::details::WrapCallback<Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()>(Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()) + 32
11  lmdb.napi.node                         0x1051093cc Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*) + 48
12  Electron Framework                     0x11ca1f314 napi_is_detached_arraybuffer + 224
13  Electron Framework                     0x1176652a0 v8::internal::Accessors::MakeAccessor(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Name>, void (*)(v8::Local<v8::Name>, v8::PropertyCallbackInfo<v8::Value> const&), void (*)(v8::Local<v8::Name>, v8::Local<v8::Value>, v8::PropertyCallbackInfo<v8::Boolean> const&)) + 11236
thomasdao commented 8 months ago

@kriszyp although I haven't received any reply from Apple yet, I believe in order for mmap to work in sandboxed or hardened runtime Mac app, it must include the flag MAP_JIT.

I found this answer on Stackoverflow and the linked source code at https://github.com/mono/mono/blob/main/mono/utils/mono-mmap.c has special handling for hardened runtime to include the flag MAP_JIT when use with mmap.

I have searched for mmap in the LMDB source code and did not the flag MAP_JIT. I'm gonna try adding this flag in the source code, but since I'm not very familiar with C, do you think it's safe to add this flag? Thank you.

kriszyp commented 8 months ago

The documentation for MAP_JIT says it is to allow execute and write permission on memory maps, but LMDB only needs read access to memory maps, so it doesn't seem like that would be an issue. And it doesn't seem like the stack trace points to memory map allocation or access, but I don't really know why that stack trace would have an issue with a malloc or free.

thomasdao commented 8 months ago

I added mm_flags |= MAP_JIT; in the mdb.c file and recompile again, and the app now crashes both locally and in TestFlight :(

I must admit that I have no idea what I'm doing. I really hope Apple will be able to answer soon.

I updated the crash log with line number, not sure if it's useful

0   libsystem_kernel.dylib                 0x198e4aa60 __pthread_kill + 8
1   libsystem_pthread.dylib                0x198e82c20 pthread_kill + 288
2   libsystem_c.dylib                      0x198d8fa20 abort + 180
3   libsystem_malloc.dylib                 0x198c9faa8 malloc_vreport + 896
4   libsystem_malloc.dylib                 0x198ca3114 malloc_report + 64
5   libsystem_malloc.dylib                 0x198cbd494 find_zone_and_free + 528
6   lmdb.napi.node                         0x10263d978 EnvWrap::closeEnv(bool) + 832 (env.cpp:713)
7   lmdb.napi.node                         0x10263edc8 EnvWrap::openEnv(int, int, char const*, char*, Compression*, int, int, unsigned long, int, unsigned int, unsigned int, char*) + 700 (env.cpp:372)
8   lmdb.napi.node                         0x10263e5e8 EnvWrap::open(Napi::CallbackInfo const&) + 1568 (env.cpp:301)
9   lmdb.napi.node                         0x102655540 Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()::operator()() const + 252 (napi-inl.h:4358)
kriszyp commented 8 months ago

If you are going to try making some source code changes, here is my suggestion: Try commenting out this line: https://github.com/kriszyp/lmdb-js/blob/master/src/env.cpp#L372 I suspect that opening the database is failing and then the attempt to clean up afterwards is trying to free some memory that never got allocated (because the database failed to open). So commenting out that line would hopefully at least show the error for why the database failed to open (instead of a spurious free() error).

thomasdao commented 8 months ago

Thank you, will do now and will update this post again when the TestFlight build is ready. @kriszyp The app now can run for a second then crashes. I saw the JavaScript error is Operation not permitted: Attempting to setup locks. The error crash in the Console app is below

System Integrity Protection: enabled

Crashed Thread:        0  CrRendererMain  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x00000001153f8010
Exception Codes:       0x0000000000000001, 0x00000001153f8010
Exception Note:        EXC_CORPSE_NOTIFY

Termination Reason:    Namespace SIGNAL, Code 11 Segmentation fault: 11
Terminating Process:   exc handler [2951]

VM Region Info: 0x1153f8010 is not in any region.  Bytes after previous region: 17  Bytes before following region: 28656
      REGION TYPE                    START - END         [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
      VM_ALLOCATE                 1153f7000-1153f8000    [    4K] rw-/rw- SM=PRV  
--->  GAP OF 0x7000 BYTES
      VM_ALLOCATE                 1153ff000-115400000    [    4K] rw-/rw- SM=PRV  

Thread 0 Crashed:: CrRendererMain Dispatch queue: com.apple.main-thread
0   node.napi.node                         0x11a7bd545 mdb_reader_check0 + 197
1   node.napi.node                         0x11a7bd45a mdb_reader_check + 90
2   node.napi.node                         0x11a772dfe EnvWrap::readerCheck(Napi::CallbackInfo const&) + 94
3   node.napi.node                         0x11a784c1c Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()::operator()() const + 332
4   node.napi.node                         0x11a784ab9 napi_value__* Napi::details::WrapCallback<Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()>(Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*)::'lambda'()) + 25
5   node.napi.node                         0x11a784a4d Napi::InstanceWrap<EnvWrap>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*) + 45
6   Electron Framework                     0x1130c274c napi_is_detached_arraybuffer + 268
kriszyp commented 8 months ago

The Operation not permitted: Attempting to setup locks is the real error, and means that LMDB was unable to open the database file or unable to lock a section of the database file. I'm pretty sure all the other errors are just downstream fallout from failures to access the database file. Is it possible that the sandboxed app is attempting to create the database file in a location that it doesn't have permission to? (it seems likely the sandboxing could restrict file access in some way). Can you verify if the (any) database file was actually created?

thomasdao commented 8 months ago

I found this answer on Github which mentioned that it's very difficult to release LMDB to the Mac App Store due to sandbox restriction.

Probably this means that I can only use LMDB for Windows and Linux and need to fallback to LevelDB on Mac.

According to the old Apple documentation:

Normally, sandboxed apps cannot use Mach IPC, POSIX semaphores and shared memory, or UNIX domain sockets (usefully). However, by specifying an entitlement that requests membership in an application group, an app can use these technologies to communicate with other members of that application group.

Note: System V semaphores are not supported in sandboxed apps.

UNIX domain sockets are straightforward; they work just like any other file.

Any semaphore or Mach port that you wish to access within a sandboxed app must be named according to a special convention:

POSIX semaphores and shared memory names must begin with the application group identifier, followed by a slash (/), followed by a name of your choosing. Mach port names must begin with the application group identifier, followed by a period (.), followed by a name of your choosing. For example, if your application group’s name is Z123456789.com.example.app-group, you might create two semaphores named Z123456789.myappgroup/rdyllwflg and Z123456789.myappgroup/bluwhtflg. You might create a Mach port named Z123456789.com.example.app-group.Port_of_Kobe.

Note: The maximum length of a POSIX semaphore name is only 31 bytes, so if you need to use POSIX semaphores, you should keep your app group names short.

thomasdao commented 8 months ago

Can you verify if the (any) database file was actually created?

Yes the database is in the internal folder of the app which the app has write permission, and I can see database files below:

kriszyp commented 7 months ago

So this is the prefix used for the posix semaphores, but I wonder if it would help if that was configurable so you could set it to the application group name?

thomasdao commented 7 months ago

I tried replace the default prefix from "/MDB" to my own app group prefix in source code and build from source again, but the app still crashes when release to TestFlight. I'm not totally sure if I did it right, but in the end I give up because the Mac App Store Sandbox is really troublesome.