Skip to content
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

feat(build): support node native modules (gyp) #964

Merged
merged 23 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [4.4.2-beta.2] - 2023-10-20

### Added

- Node Native Addon support using GYP. To enable, set `options.nativeAddon` to `gyp`.

## [4.4.2-beta.1] - 2023-10-16

### Added
Expand Down
1,215 changes: 980 additions & 235 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nw-builder",
"version": "4.4.2-beta.1",
"version": "4.4.2-beta.2",
"description": "Build NW.js desktop applications for MacOS, Windows and Linux.",
"keywords": [
"NW.js",
Expand Down Expand Up @@ -32,7 +32,9 @@
"types": "./src/index.d.ts",
"type": "module",
"files": [
"./src"
"LICENSE",
"patches",
"src"
],
"homepage": "https://github.com/nwutils/nw-builder",
"repository": {
Expand All @@ -47,7 +49,7 @@
"doc:dev": "concurrently --kill-others \"node .github/fswatch.config.js\" \"vitepress dev doc\"",
"doc:bld": "node .github/jsdoc.config.cjs && vitepress build doc",
"test:unit": "node --test test/unit/index.js",
"test:e2e": "node --test test/e2e/index.js",
"test:e2e": "node test/e2e/index.js",
"demo": "cd test/fixture && node demo.js"
},
"devDependencies": {
Expand All @@ -65,6 +67,7 @@
"cli-progress": "^3.12.0",
"compressing": "^1.10.0",
"glob": "^10.3.10",
"node-gyp": "^9.4.0",
"plist": "^3.1.0",
"rcedit": "^4.0.0",
"winston": "^3.11.0",
Expand Down
4 changes: 4 additions & 0 deletions patches/node_header.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
68c68
< 'v8_host_byteorder': '<!(python -c "import sys; print sys.byteorder")',
---
> 'v8_host_byteorder': '<!(python3 -c "import sys; print(sys.byteorder)")',
43 changes: 40 additions & 3 deletions src/build.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { exec } from "node:child_process";
import { resolve } from "node:path";
import { platform as PLATFORM, chdir } from "node:process";
import {
Expand All @@ -14,7 +15,7 @@ import rcedit from "rcedit";
import plist from "plist";

import { log } from "./log.js";
import { exec } from "node:child_process";

/**
* References:
* https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
Expand Down Expand Up @@ -142,21 +143,31 @@ import { exec } from "node:child_process";
* @param {string | string[]} files Array of NW app files
* @param {string} nwDir Directory to hold NW binaries
* @param {string} outDir Directory to store build artifacts
* @param {string} cacheDir Directory to store NW.js related binaries
* @param {string} version NW.js runtime version
* @param {"linux" | "osx" | "win"} platform Platform is the operating system type
* @param {"ia32" | "x64" | "arm64"} arch NW supported architectures
* @param {"zip" | boolean} zip Specify if the build artifacts are to be zipped
* @param {boolean | string | object} managedManifest Managed Manifest mode
* @param {string} nwPkg NW.js manifest file
* @param {false | "gyp"} nativeAddon Rebuild Node Native Addon
* @param {string} nodeVersion Version of Node included in NW.js release
* @param {LinuxRc | OsxRc | WinRc} app Multi platform configuration options
* @return {Promise<undefined>}
*/
export async function build(
files,
nwDir,
outDir,
cacheDir,
version,
platform,
arch,
zip,
managedManifest,
nwPkg,
nativeAddon,
nodeVersion,
app,
) {
log.debug(`Remove any files at ${outDir} directory`);
Expand Down Expand Up @@ -197,7 +208,7 @@ export async function build(
let manifest = undefined;

if (
typeof managedManifest === "boolean" ||
managedManifest === true ||
typeof managedManifest === "object" ||
typeof managedManifest === "string"
) {
Expand All @@ -217,7 +228,9 @@ export async function build(
}

log.debug("Remove development dependencies.");
manifest.devDependencies = undefined;
if (manifest.devDependencies) {
manifest.devDependencies = undefined;
}
log.debug("Detect Node package manager.");
manifest.packageManager = manifest.packageManager ?? "npm@*";

Expand Down Expand Up @@ -411,6 +424,30 @@ export async function build(
}
}

if (nativeAddon === "gyp") {
let nodePath = resolve(cacheDir, `node-v${version}-${platform}-${arch}`);
log.debug("Native Node Addon (GYP) is enabled.");
chdir(
resolve(
outDir,
platform !== "osx"
? "package.nw"
: "nwjs.app/Contents/Resources/app.nw",
),
);

log.debug("Rebuilding of Native Node module started");
exec(
`node-gyp rebuild --target=${nodeVersion} --nodedir=${nodePath}`,
(error) => {
if (error !== null) {
log.error(error);
}
},
);
log.debug("Rebuilding of Native Node module ended");
}

if (zip !== false) {
if (zip === true || zip === "zip") {
await compressing.zip.compressDir(outDir, `${outDir}.zip`);
Expand Down
138 changes: 126 additions & 12 deletions src/get.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawnSync } from "node:child_process";
import { exec, spawnSync } from "node:child_process";
import { createWriteStream, existsSync } from "node:fs";
import { mkdir, readdir, rm, rmdir } from "node:fs/promises";
import { mkdir, readdir, rename, rm } from "node:fs/promises";
import { get as getRequest } from "node:https";
import { resolve } from "node:path";
import { arch as ARCH, platform as PLATFORM, exit as EXIT } from "node:process";
Expand Down Expand Up @@ -63,6 +63,7 @@ import { ARCH_KV, PLATFORM_KV, replaceFfmpeg } from "./util.js";
* @param {string} options.cacheDir Cache directory path. Defaults to "./cache"
* @param {boolean} options.cache If false, remove cache before download. Defaults to true.
* @param {boolean} options.ffmpeg If true, ffmpeg is not downloaded. Defaults to false.
* @param {false | "gyp"} options.nativeAddon Rebuilds native modules. Defaults to false.
* @return {Promise<void>}
*/
export async function get({
Expand All @@ -74,6 +75,7 @@ export async function get({
cacheDir = "./cache",
cache = true,
ffmpeg = false,
nativeAddon = false,
}) {
await get_nwjs({
version,
Expand All @@ -95,6 +97,15 @@ export async function get({
cache,
});
}
if (nativeAddon === "gyp") {
await getNodeHeaders({
version: version,
platform: platform,
arch: arch,
cacheDir: cacheDir,
cache: cache,
});
}
}

/**
Expand All @@ -121,7 +132,7 @@ async function get_nwjs({
cacheDir = "./cache",
cache = true,
}) {
log.debug(`Start getting binaries`);
log.debug(`Start NW.js getting binaries`);
let nwCached = true;
const nwDir = resolve(
cacheDir,
Expand All @@ -147,22 +158,22 @@ async function get_nwjs({

// If options.cache is false, remove cache.
if (cache === false) {
log.debug(`Removing existing binaries`);
rmdir(nwDir, { recursive: true, force: true });
log.debug(`Removing existing NW.js binaries`);
await rm(nwDir, { recursive: true, force: true });
}

// Check if cache exists.
try {
await readdir(nwDir);
log.debug(`Found existing NW.js binaries`);
} catch (error) {
log.debug(`No existing binaries`);
log.debug(`No NW.js existing binaries`);
nwCached = false;
}

// If not cached, then download.
if (nwCached === false) {
log.debug(`Downloading binaries`);
log.debug(`Downloading NW.js binaries`);
await mkdir(nwDir, { recursive: true });

const stream = createWriteStream(out);
Expand Down Expand Up @@ -228,13 +239,13 @@ async function get_nwjs({

// Remove compressed file after download and decompress.
return request.then(async () => {
log.debug(`Binary decompressed starting removal`);
log.debug(`NW.js binary decompressed starting removal`);

await rm(
resolve(cacheDir, `nw.${platform === "linux" ? "tgz" : "zip"}`),
{ recursive: true, force: true },
);
log.debug(`Binary zip removed`);
log.debug(`NW.js binary zip removed`);
});
}
}
Expand Down Expand Up @@ -262,7 +273,7 @@ async function get_ffmpeg({
cacheDir = "./cache",
cache = true,
}) {
log.debug(`Start getting binaries`);
log.debug(`Start getting FFmpeg binaries`);
const nwDir = resolve(
cacheDir,
`nwjs${flavor === "sdk" ? "-sdk" : ""}-v${version}-${platform}-${arch}`,
Expand All @@ -277,7 +288,7 @@ async function get_ffmpeg({

// If options.cache is false, remove cache.
if (cache === false) {
log.debug(`Removing existing binaries`);
log.debug(`Removing existing FFmpeg binaries`);
await rm(out, {
recursive: true,
force: true,
Expand All @@ -292,7 +303,7 @@ async function get_ffmpeg({
return;
}

log.debug(`Downloading FFMPEG`);
log.debug(`Downloading FFmpeg binary`);
const stream = createWriteStream(out);
const request = new Promise((resolve, reject) => {
getRequest(url, (response) => {
Expand Down Expand Up @@ -344,3 +355,106 @@ async function get_ffmpeg({
}
});
}

/**
* Get Node headers
*
* @param {object} options Get mode options
* @param {string} options.version NW.js runtime version. Defaults to "latest".
* @param {"linux" | "osx" | "win"} options.platform Target platform. Defaults to host platform.
* @param {"ia32" | "x64" | "arm64"} options.arch Target architecture. Defaults to host architecture.
* @param {string} options.cacheDir Cache directory path. Defaults to "./cache"
* @param {string} options.cache If false, remove cache before download. Defaults to true.
* @return {Promise<void>}
*/
export async function getNodeHeaders({
version,
platform,
arch,
cacheDir,
cache = true,
}) {
const bar = new progress.SingleBar({}, progress.Presets.rect);
const out = resolve(
cacheDir,
`headers-v${version}-${platform}-${arch}.tar.gz`,
);

// If options.cache is false, remove cache.
if (cache === false) {
log.debug(`Removing existing Node headers`);
await rm(out, {
recursive: true,
force: true,
});
log.debug(`Node headers tgz cache removed`);
}

if (existsSync(out) === true) {
log.debug(`Found existing Node headers cache`);
await compressing.tgz.uncompress(out, cacheDir);
await rm(resolve(cacheDir, `node-v${version}-${platform}-${arch}`), {
recursive: true,
force: true,
});
await rename(
resolve(cacheDir, "node"),
resolve(cacheDir, `node-v${version}-${platform}-${arch}`),
);

exec(
"patch " +
resolve(
cacheDir,
`node-v${version}-${platform}-${arch}`,
"common.gypi",
) +
" " +
resolve("..", "..", "patches", "node_header.patch"),
(error) => {
log.error(error);
},
);

return;
}

const stream = createWriteStream(out);
const request = new Promise((resolve, reject) => {
const urlBase = "https://dl.nwjs.io/";
const url = `${urlBase}/v${version}/nw-headers-v${version}.tar.gz`;
getRequest(url, (response) => {
log.debug(`Response from ${url}`);
let chunks = 0;
bar.start(Number(response.headers["content-length"]), 0);
response.on("data", async (chunk) => {
chunks += chunk.length;
bar.increment();
bar.update(chunks);
});

response.on("error", (error) => {
reject(error);
});

response.on("end", () => {
log.debug(`FFMPEG fully downloaded`);
bar.stop();
resolve();
});

response.pipe(stream);
});
});

return request
.then(async () => {
await compressing.tgz.uncompress(out, cacheDir);
})
.then(async () => {
await rename(
resolve(cacheDir, "node"),
resolve(cacheDir, `node-v${version}-${platform}-${arch}`),
);
});
}
Loading