Several issues trying to snap NodeJS package

I ended up in an unexpected world of pain while trying to create a snap of a simple NodeJS package: https://github.com/blgm/jfq. Locally, the code installs without any issues, but for some reason snapcraft appears to have quite an aversion to it. Mostly for documentation’s sake, I’ll mention the few issues that I encountered, and via a meandering path managed to finally tackle.

Issue 1: Missing executable due to build script not being invoked

For a while I would consistently get the following message during the build process:

FileNotFoundError: [Errno 2] No such file or directory: '/build/jfq/parts/jfq/install/bin/jfq.js'

This was happening both on builds running on build.snapcraft.io as well as an equivalent error when running snapcraft locally. It just looked like the executable wasn’t being put in the right place

Eventually I worked out that the “build” script

rollup -c && chmod a+x ./bin/jfq.js

defined in the original package.json isn’t actually being run - and the rollup command is what actually copies over the executable js file. This is fixed by adding the same script under a “prepare” key in config.json. Snapcraft then does run it, and on my local machine the snap (from my fork) builds successfully.

It still wasn’t succeeding on build.snapcraft.io.

Issue 2: Permissions issue during build

Once the build/prepare command was in place, the following started appearing:

npm WARN lifecycle jfq@1.2.0~prepare: cannot run in wd jfq@1.2.0 rollup -c && chmod a+x ./bin/jfq.js (wd=/build/jfq/parts/jfq/src)

This appears to be a fairly well-documented situation that occurs when npm is run as root (as I’m guessing it is in the build container), which causes it to drop privileges, and it therefore cannot run commands like this.

The most common solution seems to be to add the --unsafe-perm command line parameter. If we don’t want to override the nodejs plugin’s build command, in snapcraft this can be addressed with an environment variable

parts:
  jfq:
    plugin: nodejs
    nodejs-package-manager: npm
    source-type: local
    source: .
    build-environment:
      - npm_config_unsafe_perm: 'true'

But now we get to…

Issue 3: Node appears to run the build script at the wrong time/place

Below are some extracts from the log. The prepare command is run three times (not entirely sure why?). The first two times it succeeds, and copies over the executable. But on the last go, rollup appears to no longer be available. I was at a loss as to how to tackle this. In the end the solution appeared to be setting the script that invokes rollup to be under the “prepack” stage rather than the “prepare” (or “prepublish”) stage.

I don’t know enough about the NPM lifecycle to truly understand what’s going on here, but I thought I’d share in case this is helpful.

Anyway, I’m still very puzzled as to just how challenging this turned out to be, given that the package installs without a hitch in a regular NPM environment.

> jfq@1.2.0 prepare /build/jfq/parts/jfq/src
> rollup -c


src/jfq.js → bin/jfq.js...
created bin/jfq.js in 1.7s

[...]

> jfq@1.2.0 prepare /build/jfq/parts/jfq/src
> rollup -c


src/jfq.js → bin/jfq.js...
created bin/jfq.js in 1.2s
npm notice 
npm notice 📦  jfq@1.2.0
npm notice === Tarball Contents === 
npm notice 1.5kB package.json
npm notice 1.1kB LICENSE     
npm notice 1.6kB README.md   
npm notice 5.0kB bin/jfq.js  
npm notice === Tarball Details === 
npm notice name:          jfq                                     
npm notice version:       1.2.0                                   
npm notice filename:      jfq-1.2.0.tgz                           
npm notice package size:  3.8 kB                                  
npm notice unpacked size: 9.2 kB                                  
npm notice shasum:        9e9c56c847bccbd0d601863ebfdd8501c8df24aa
npm notice integrity:     sha512-cxz1c4HetU3oT[...]2++fuiGSxJQ2Q==
npm notice total files:   4                                       
npm notice 
jfq-1.2.0.tgz

[...]

> jfq@1.2.0 prepare /build/jfq/parts/jfq/src/package
> rollup -c && chmod a+x ./bin/jfq.js

Error: ENOENT: no such file or directory, lstat '/build/jfq/parts/jfq/src/package/rollup.config.js'
    at Object.realpathSync (fs.js:1657:15)
    at runRollup (/build/jfq/parts/jfq/src/package/node_modules/rollup/dist/bin/rollup:1312:29)
    at Object.<anonymous> (/build/jfq/parts/jfq/src/package/node_modules/rollup/dist/bin/rollup:1363:5)
    at Module._compile (module.js:653:30)
    at Object.Module._extensions..js (module.js:664:10)
    at Module.load (module.js:566:32)
    at tryModuleLoad (module.js:506:12)
    at Function.Module._load (module.js:498:3)
    at Function.Module.runMain (module.js:694:10)
    at startup (bootstrap_node.js:204:16)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! jfq@1.2.0 build: `rollup -c && chmod a+x ./bin/jfq.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the jfq@1.2.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2019-09-19T22_10_17_814Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! jfq@1.2.0 prepublish: `npm run build`
npm ERR! Exit status 1

I spend a lot of time to fix probably the same 2 and 3 issues. The solution for me was:

    build-environment:
        - SUDO_UID: '0'
        - SUDO_GID: '0'
        - SUDO_USER: 'root'

You could check for more details here:

1 Like