Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

pkg로 바이너리를 컴파일할 때 Native 애드온을 같이 사용할 때의 오류

며칠 전에 pkgNode.js를 하나의 파일로 패키징하는 방법에 관한 글을 썼는데 pkg를 사용할 때 주의할 점이 있다.

Node.js는 크게 JavaScirpt로 작성한 모듈과 C++ 애드온으로 작성한 네이티브 모듈이 있다. JavaScript로만 작성된 경우 V8 위에서 실행되므로 어디서나 같게 동작하지만 네이티브 모듈 같은 경우는 macOS나 Windows, Linux 등 플랫폼에 맞게 컴파일을 해야 제대로 동작한다. 각 플랫폼에서 일관된 인터페이스로 빌드 해주는 도구가 gyp이다.

각 플랫폼에서 컴파일해야 한다는 말이 담고 있는 의미대로 pkg로 애플리케이션을 여러 플랫폼에 맞게 하나의 바이너리로 만들 때 이 네이티브 애드온을 사용하고 있으면 문제가 된다. 이를 확인해 보자.

var bcrypt = require('bcrypt');

const saltRounds = 10;
const myPlaintextPassword = 's0/\/\P4$$w0rD';
const someOtherPlaintextPassword = 'not_bacon';

bcrypt.genSalt(saltRounds, function(err, salt) {
  bcrypt.hash(myPlaintextPassword, salt, function(err, hash) {
    console.log(hash);
  });
});

위 코드는 네이티브 애드온인 bcrypt를 사용해서 암호화한 비밀번호를 출력하는 간단한 코드다. pkg가 설치되어 있을 때 pkg . --out-path=dist로 컴파일하면 dist 디렉터리 아래 플랫폼별로 컴파일된 바이너리가 생성된다.

dist/
├── pkg-test-linux
├── pkg-test-macos
└── pkg-test-win.exe

이 컴파일된 파일을 실행하면(테스트 환경은 macOS이다.) 다음과 같이 정상적으로 실행된다.

$ ./dist/pkg-test-macos
$2b$10$vffib/49AN2ISuuTAxavuuAS7W/IEtJEzQc0emLbcQHQZdSn17Yte

이렇게 보면 아무런 문제가 없는 것 같지만, 이 파일을 프로젝트 디렉터리가 아닌 다른 곳으로 이동한 뒤에 실행하면 다음과 같은 오류가 발생한다.

$ ./pkg-test-macos
pkg/prelude/bootstrap.js:1172
      throw error;
      ^

Error: Cannot find module '/snapshot/pkg-test/node_modules/bcrypt/lib/binding/bcrypt_lib.node'
1) If you want to compile the package/file into executable, please pay attention to compilation warnings and specify a literal in 'require' call. 2) If you don't want to compile the package/file into executable and want to 'require' it from filesystem (likely plugin), specify an absolute path in 'require' call using process.cwd() or process.execPath.
    at Function.Module._resolveFilename (module.js:534:15)
    at Function.Module._resolveFilename (pkg/prelude/bootstrap.js:1269:46)
    at Function.Module._load (module.js:464:25)
    at Module.require (module.js:577:17)
    at Module.require (pkg/prelude/bootstrap.js:1153:31)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/snapshot/pkg-test/node_modules/bcrypt/bcrypt.js:6:16)
    at Module._compile (pkg/prelude/bootstrap.js:1243:22)
    at Object.Module._extensions..js (module.js:644:10)
    at Module.load (module.js:552:32)

이는 Linux에서 실행해도 마찬가지다. 다음과 같은 Dockerfile을 만들어 보자.

FROM debian:jessie

ADD ./dist/pkg-test-linux .

CMD ./pkg-test-linux

이를 실행하면 앞과 같은 오류를 볼 수 있다.

$ docker build -t pkg-test .
Sending build context to Docker daemon  117.9MB
Step 1/3 : FROM debian:jessie
 ---> 4eb8376dc2a3
Step 2/3 : ADD ./dist/pkg-test-linux .
 ---> Using cache
 ---> 081da6150858
Step 3/3 : CMD ./pkg-test-linux
 ---> Using cache
 ---> ec3f44640e61
Successfully built ec3f44640e61
Successfully tagged pkg-test:latest

$ docker run --rm -it pkg-test:latest
pkg/prelude/bootstrap.js:1172
      throw error;
      ^

Error: Cannot find module '/snapshot/pkg-test/node_modules/bcrypt/lib/binding/bcrypt_lib.node'
1) If you want to compile the package/file into executable, please pay attention to compilation warnings and specify a literal in 'require' call. 2) If you don't want to compile the package/file into executable and want to 'require' it from filesystem (likely plugin), specify an absolute path in 'require' call using process.cwd() or process.execPath.
    at Function.Module._resolveFilename (module.js:534:15)
    at Function.Module._resolveFilename (pkg/prelude/bootstrap.js:1269:46)
    at Function.Module._load (module.js:464:25)
    at Module.require (module.js:577:17)
    at Module.require (pkg/prelude/bootstrap.js:1153:31)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/snapshot/pkg-test/node_modules/bcrypt/bcrypt.js:6:16)
    at Module._compile (pkg/prelude/bootstrap.js:1243:22)
    at Object.Module._extensions..js (module.js:644:10)
    at Module.load (module.js:552:32)

결국 /snapshot/pkg-test/node_modules/bcrypt/lib/binding/bcrypt_lib.node 파일을 찾지 못한다는 오류이다. 네이티브 애드온은 플랫폼에 맞게 컴파일되고 나면 .node 파일로 생성된다. 이 글을 쓰는 환경은 macOS인데 npm 모듈이 설치된 node_modules 아래를 보면 bcrypt_lib.node 파일이 생성된 것을 볼 수 있다.

$ ls -lh node_modules/bcrypt/lib/binding/bcrypt_lib.node
-rwxr-xr-x  1 outsider  staff    48K  4 21 02:22 node_modules/bcrypt/lib/binding/bcrypt_lib.node

Native addons (.node files) use is supported, but packaging .node files inside the executable is not resolved yet. You have to deploy native addons used by your project to the same directory as the executable.

pkg의 문서를 보면 위처럼 네이티브 애드온의 .node 파일을 패키징된 파일과 같은 디렉터리 안에 넣어야 한다고 나와 있다. 아직 네이티브 애드온까지 하나의 바이너리로 만드는 기능은 지원하지 않는다. 현재 pkg의 버전은 4.3.1이다.

위 파일을 프로젝트 디렉터리 외부로 pkg-test-macos 파일을 복사한 위치에 복사해 보자. 다시 pkg-test-macos를 실행하면 정상적으로 실행되는 것을 볼 수 있다.

$ ./pkg-test-macos
$2b$10$ZiRPqivssFsuMVrBgHSNauN9M9buvv40SXELNB1nYtkB.nd73U.PG

네이티브 애드온을 사용할 때의 문제는 파악했지만, 이는 현실적으로 제대로 활용하기는 어려워 보인다. macOS, Linux, Windows를 타겟으로 바이너리를 컴파일했다고 했을 때 지금 환경이 macOS라면 이 .node 파일은 macOS 환경에 맞춰서 빌드된 파일일 뿐이다. 그래서 위 3개 플랫폼에 맞게 배포하려면 각 플랫폼에서 컴파일한 .node 파일이 필요하고 이를 모두 배포한 뒤 사용자가 바이너리와 .node 파일을 같은 디렉터리 안에 넣고 사용해야 한다.

자동화를 한다면 플랫폼별 파일을 못 만들어 낼 것은 아니지만 회사나 조직 등 통제된 환경에서 배포하는 게 아니라 일반 사용자를 대상으로 배포한다면 위의 설명처럼 같은 디렉터리 내에 파일을 두고 사용하라고 안내하기는 쉽지 않은 일이라고 생각한다.

그래서 pkg를 사용하려고 할 때 네이티브 애드온을 사용한다면 이런 부분을 고려해봐야 한다. 배포 안내를 잘 하거나 네이티브 애드온을 최대한 자제하거나 하는 등의 선택이 필요해 보인다.

2018/06/03 04:08 2018/06/03 04:08