diff --git a/README.md b/README.md index 5216b45a4..aac1113b5 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,7 @@ nwbuild({ - A commit's first line should be formatted as `[optional scope]: `. - A commit's body should have a description of changes in bullet points followed by any links it references or issues it fixes or closes. It may include an optional `Notes: ...` section to provide additional context on why the PR is being merged when it doesn't seem like it should. - Google's Release Please Action is used to update the changelog, bump the package version and generate GitHub releases. +- NPM Publish Action publishes to `npm` if there is a version bump. ## Roadmap @@ -297,8 +298,9 @@ nwbuild({ ### Chores - chore(cli): migrate from `yargs` to `commander` +- chore(get): investigate [how symlinks are identified](https://github.com/overlookmotel/yauzl-promise/issues/39) and remove the workaround where they are created manually - chore(get): verify sha checksum for downloads -- chore(util): factor out file paths as constant variables +- chore: annotate file paths as `fs.PathLike` instead of `string`. - chore(bld): factor out core build step - chore(bld): factor out linux config - chore(bld): factor out macos config @@ -307,6 +309,7 @@ nwbuild({ - chore(bld): factor out compressing - chore(bld): factor out managed manifest - chore(bld): move `.desktop` entry file logic to `create-desktop-shortcuts` package +- chore(util): factor out file paths as constant variables ## FAQ diff --git a/src/get.js b/src/get.js deleted file mode 100644 index 455da9872..000000000 --- a/src/get.js +++ /dev/null @@ -1,232 +0,0 @@ -import fs from "node:fs"; -import https from "node:https"; -import path from "node:path"; - -import progress from "cli-progress"; -import tar from "tar"; - -import decompress, { unzip } from "./get/decompress.js"; -import nw from "./get/nw.js"; - -import util from "./util.js"; - -/** - * @typedef {object} GetOptions - * @property {string | "latest" | "stable" | "lts"} [version = "latest"] Runtime version - * @property {"normal" | "sdk"} [flavor = "normal"] Build flavor - * @property {"linux" | "osx" | "win"} [platform] Target platform - * @property {"ia32" | "x64" | "arm64"} [arch] Target arch - * @property {string} [downloadUrl = "https://dl.nwjs.io"] Download server - * @property {string} [cacheDir = "./cache"] Cache directory - * @property {boolean} [cache = true] If false, remove cache and redownload. - * @property {boolean} [ffmpeg = false] If true, ffmpeg is not downloaded. - * @property {false | "gyp"} [nativeAddon = false] Rebuild native modules - */ - -/** - * Get binaries. - * - * @async - * @function - * @param {GetOptions} options Get mode options - * @return {Promise} - */ -async function get(options) { - - const cacheDirExists = await util.fileExists(options.cacheDir); - if (cacheDirExists === false) { - await fs.promises.mkdir(options.cacheDir, { recursive: true }); - } - - let nwFilePath = path.resolve( - options.cacheDir, - `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}.${options.platform === "linux" ? "tar.gz" : "zip" - }`, - ); - - let nwDirPath = path.resolve( - options.cacheDir, - `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, - ); - - if (options.cache === false) { - await fs.promises.rm(nwFilePath, { - recursive: true, - force: true, - }); - } - - if (util.fileExists(nwFilePath)) { - nwFilePath = await nw(options.downloadUrl, options.version, options.flavor, options.platform, options.arch, options.cacheDir); - } - - await fs.promises.rm(nwDirPath, { recursive: true, force: true }); - - await decompress(nwFilePath, options.cacheDir); - - if (options.platform === "osx") { - await createSymlinks(options); - } - - if (options.ffmpeg === true) { - await getFfmpeg(options); - } - if (options.nativeAddon === "gyp") { - await getNodeHeaders(options); - } -} - -const getFfmpeg = async (options) => { - const nwDir = path.resolve( - options.cacheDir, - `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, - ); - const bar = new progress.SingleBar({}, progress.Presets.rect); - - // If options.ffmpeg is true, then download ffmpeg. - options.downloadUrl = "https://github.com/nwjs-ffmpeg-prebuilt/nwjs-ffmpeg-prebuilt/releases/download"; - let url = `${options.downloadUrl}/${options.version}/${options.version}-${options.platform}-${options.arch}.zip`; - const out = path.resolve(options.cacheDir, `ffmpeg-v${options.version}-${options.platform}-${options.arch}.zip`); - - // If options.cache is false, remove cache. - if (options.cache === false) { - await fs.promises.rm(out, { - recursive: true, - force: true, - }); - } - - // Check if cache exists. - if (fs.existsSync(out) === true) { - await util.unzip(out, nwDir); - return; - } - - const stream = fs.createWriteStream(out); - const request = new Promise((res, rej) => { - https.get(url, (response) => { - // For GitHub releases and mirrors, we need to follow the redirect. - url = response.headers.location; - - https.get(url, (response) => { - let chunks = 0; - bar.start(Number(response.headers["content-length"]), 0); - response.on("data", (chunk) => { - chunks += chunk.length; - bar.increment(); - bar.update(chunks); - }); - - response.on("error", (error) => { - rej(error); - }); - - response.on("end", () => { - bar.stop(); - res(); - }); - - response.pipe(stream); - }); - - response.on("error", (error) => { - rej(error); - }); - }); - }); - - // Remove compressed file after download and decompress. - await request; - await unzip(out, nwDir); - await util.replaceFfmpeg(options.platform, nwDir); -} - -const getNodeHeaders = async (options) => { - const bar = new progress.SingleBar({}, progress.Presets.rect); - const out = path.resolve( - options.cacheDir, - `headers-v${options.version}-${options.platform}-${options.arch}.tar.gz`, - ); - - // If options.cache is false, remove cache. - if (options.cache === false) { - await fs.promises.rm(out, { - recursive: true, - force: true, - }); - } - - if (fs.existsSync(out) === true) { - await tar.extract({ - file: out, - C: options.cacheDir - }); - await fs.promises.rm(path.resolve(options.cacheDir, `node-v${options.version}-${options.platform}-${options.arch}`), { - recursive: true, - force: true, - }); - await fs.promises.rename( - path.resolve(options.cacheDir, "node"), - path.resolve(options.cacheDir, `node-v${options.version}-${options.platform}-${options.arch}`), - ); - return; - } - - const stream = fs.createWriteStream(out); - const request = new Promise((res, rej) => { - const url = `${options.downloadUrl}/v${options.version}/nw-headers-v${options.version}.tar.gz`; - https.get(url, (response) => { - let chunks = 0; - bar.start(Number(response.headers["content-length"]), 0); - response.on("data", (chunk) => { - chunks += chunk.length; - bar.increment(); - bar.update(chunks); - }); - - response.on("error", (error) => { - rej(error); - }); - - response.on("end", () => { - bar.stop(); - res(); - }); - - response.pipe(stream); - }); - }); - - await request; - await tar.extract({ - file: out, - C: options.cacheDir - }); - await fs.promises.rename( - path.resolve(options.cacheDir, "node"), - path.resolve(options.cacheDir, `node-v${options.version}-${options.platform}-${options.arch}`), - ); -} - -const createSymlinks = async (options) => { - let frameworksPath = path.resolve(process.cwd(), options.cacheDir, `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, "nwjs.app", "Contents", "Frameworks", "nwjs Framework.framework") - // Allow resolve cacheDir from another directory for prevent crash - if (!fs.lstatSync(frameworksPath).isDirectory()) { - frameworksPath = path.resolve(options.cacheDir, `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, "nwjs.app", "Contents", "Frameworks", "nwjs Framework.framework") - } - const symlinks = [ - path.join(frameworksPath, "Helpers"), - path.join(frameworksPath, "Libraries"), - path.join(frameworksPath, "nwjs Framework"), - path.join(frameworksPath, "Resources"), - path.join(frameworksPath, "Versions", "Current"), - ]; - for await (const symlink of symlinks) { - const buffer = await fs.promises.readFile(symlink); - const link = buffer.toString(); - await fs.promises.rm(symlink); - await fs.promises.symlink(link, symlink); - } -}; - -export default get; diff --git a/src/get/decompress.js b/src/get/decompress.js index 821401428..53ddc1b5e 100644 --- a/src/get/decompress.js +++ b/src/get/decompress.js @@ -32,18 +32,17 @@ export default async function decompress(filePath, cacheDir) { * @param {string} cacheDir - directory to unzip in * @return {Promise} */ -export async function unzip(zippedFile, cacheDir) { +async function unzip(zippedFile, cacheDir) { await unzipInternal(zippedFile, cacheDir, false).then(() => { unzipInternal(zippedFile, cacheDir, true); }) } /** - * Method for unzip with symlink in theoretical + * Method for unzip with symlink. Workaround for not being able to handle symlinks. Tracking in linked issue. * * @async * @function - * @param unzipSymlink * @param {string} zippedFile - file path to .zip file * @param {string} cacheDir - directory to unzip in * @param {boolean} unzipSymlink - Using or not symlink diff --git a/src/get/index.js b/src/get/index.js new file mode 100644 index 000000000..302de160a --- /dev/null +++ b/src/get/index.js @@ -0,0 +1,240 @@ +import fs from "node:fs"; +import path from "node:path"; + +import decompress from "./decompress.js"; +import ffmpeg from "./ffmpeg.js"; +import node from "./node.js"; +import nw from "./nw.js"; + +import util from "../util.js"; + +/** + * @typedef {object} GetOptions + * @property {string | "latest" | "stable" | "lts"} [version = "latest"] Runtime version + * @property {"normal" | "sdk"} [flavor = "normal"] Build flavor + * @property {"linux" | "osx" | "win"} [platform] Target platform + * @property {"ia32" | "x64" | "arm64"} [arch] Target arch + * @property {string} [downloadUrl = "https://dl.nwjs.io"] Download server + * @property {string} [cacheDir = "./cache"] Cache directory + * @property {boolean} [cache = true] If false, remove cache and redownload. + * @property {boolean} [ffmpeg = false] If true, ffmpeg is not downloaded. + * @property {false | "gyp"} [nativeAddon = false] Rebuild native modules + */ + +/** + * Get binaries. + * + * @async + * @function + * @param {GetOptions} options Get mode options + * @return {Promise} + */ +async function get(options) { + + /** + * If `options.cacheDir` exists, then `true`. Otherwise, it is `false`. + * + * @type {boolean} + */ + const cacheDirExists = await util.fileExists(options.cacheDir); + if (cacheDirExists === false) { + await fs.promises.mkdir(options.cacheDir, { recursive: true }); + } + + /** + * File path to compressed binary. + * + * @type {string} + */ + let nwFilePath = path.resolve( + options.cacheDir, + `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}.${options.platform === "linux" ? "tar.gz" : "zip" + }`, + ); + + /** + * File path to directory which contain NW.js and related binaries. + * + * @type {string} + */ + let nwDirPath = path.resolve( + options.cacheDir, + `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, + ); + + // If `options.cache` is false, then remove the compressed binary. + if (options.cache === false) { + await fs.promises.rm(nwFilePath, { + recursive: true, + force: true, + }); + } + + // We remove the nwDir to prevent the edge case where you download with ffmpeg flag enabled + // but want a subsequent build with ffmpeg flag disabled. By removing the directory and + // decompressing it again, we prevent the community ffmpeg files from being left over. + // This is important since the community ffmpeg builds have specific licensing constraints. + await fs.promises.rm(nwDirPath, { recursive: true, force: true }); + + /** + * If the compressed binary exists, then `true`. Otherwise, it is `false`. + * + * @type {boolean} + */ + const nwFilePathExists = await util.fileExists(nwFilePath); + if (nwFilePathExists === false) { + nwFilePath = await nw(options.downloadUrl, options.version, options.flavor, options.platform, options.arch, options.cacheDir); + } + + await decompress(nwFilePath, options.cacheDir); + + // TODO: remove once linked issue is resolved. + // https://github.com/overlookmotel/yauzl-promise/issues/39 + if (options.platform === "osx") { + await createSymlinks(options); + } + + if (options.ffmpeg === true) { + + /** + * File path to compressed binary which contains community FFmpeg binary. + * + * @type {string} + */ + let ffmpegFilePath = path.resolve( + options.cacheDir, + `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}.zip`, + ); + + // If `options.cache` is false, then remove the compressed binary. + if (options.cache === false) { + await fs.promises.rm(ffmpegFilePath, { + recursive: true, + force: true, + }); + } + + /** + * If the compressed binary exists, then `true`. Otherwise, it is `false`. + * + * @type {boolean} + */ + const ffmpegFilePathExists = await util.fileExists(ffmpegFilePath); + if (ffmpegFilePathExists === false) { + ffmpegFilePath = await ffmpeg(options.downloadUrl, options.version, options.platform, options.arch, options.cacheDir); + } + + await decompress(ffmpegFilePath, options.cacheDir); + + /** + * Platform dependant file name of FFmpeg binary. + * + * @type {string} + */ + let ffmpegFileName = ""; + + if (options.platform === "linux") { + ffmpegFileName = "libffmpeg.so"; + } else if (options.platform === "win") { + ffmpegFileName = "ffmpeg.dll"; + } else if (options.platform === "osx") { + ffmpegFileName = "libffmpeg.dylib"; + } + + /** + * File path to platform specific FFmpeg file. + * + * @type {string} + */ + let ffmpegBinaryPath = path.resolve(nwDirPath, ffmpegFileName); + + /** + * File path of where FFmpeg will be copied to. + * + * @type {string} + */ + let ffmpegBinaryDest = ""; + + if (options.platform === "linux") { + ffmpegBinaryDest = path.resolve(nwDirPath, "lib", ffmpegFileName); + } else if (options.platform === "win") { + // Extracted file is already in the correct path + } else if (options.platform === "osx") { + ffmpegBinaryDest = path.resolve( + nwDirPath, + "nwjs.app", + "Contents", + "Frameworks", + "nwjs Framework.framework", + "Versions", + "Current", + ffmpegFileName, + ); + } + + await fs.promises.copyFile(ffmpegBinaryPath, ffmpegBinaryDest); + + } + + if (options.nativeAddon === "gyp") { + + /** + * File path to NW'js Node headers tarball. + * + * @type {string} + */ + let nodeFilePath = path.resolve( + options.cacheDir, + `headers-v${options.version}.tar.gz`, + ); + + // If `options.cache` is false, then remove the compressed binary. + if (options.cache === false) { + await fs.promises.rm(nodeFilePath, { + recursive: true, + force: true, + }); + } + + /** + * If the compressed binary exists, then `true`. Otherwise, it is `false`. + * + * @type {boolean} + */ + const nodeFilePathExists = await util.fileExists(nodeFilePath); + if (nodeFilePathExists === false) { + nodeFilePath = await node(options.downloadUrl, options.version, options.cacheDir); + } + + await decompress(nodeFilePath, options.cacheDir); + + } +} + +/** + * Workaround for manually creating symbolic links for MacOS builds. + * + * @param {object} options - options config + */ +const createSymlinks = async (options) => { + let frameworksPath = path.resolve(process.cwd(), options.cacheDir, `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, "nwjs.app", "Contents", "Frameworks", "nwjs Framework.framework") + // Allow resolve cacheDir from another directory for prevent crash + if (!fs.lstatSync(frameworksPath).isDirectory()) { + frameworksPath = path.resolve(options.cacheDir, `nwjs${options.flavor === "sdk" ? "-sdk" : ""}-v${options.version}-${options.platform}-${options.arch}`, "nwjs.app", "Contents", "Frameworks", "nwjs Framework.framework") + } + const symlinks = [ + path.join(frameworksPath, "Helpers"), + path.join(frameworksPath, "Libraries"), + path.join(frameworksPath, "nwjs Framework"), + path.join(frameworksPath, "Resources"), + path.join(frameworksPath, "Versions", "Current"), + ]; + for await (const symlink of symlinks) { + const buffer = await fs.promises.readFile(symlink); + const link = buffer.toString(); + await fs.promises.rm(symlink); + await fs.promises.symlink(link, symlink); + } +}; + +export default get; diff --git a/src/index.js b/src/index.js index d166f5dce..ea47c6c83 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ import fs from "node:fs"; import fsm from "node:fs/promises"; import bld from "./bld.js"; -import get from "./get.js"; +import get from "./get/index.js"; import run from "./run.js"; import util from "./util.js"; diff --git a/src/util.js b/src/util.js index 00550483a..be806b24f 100644 --- a/src/util.js +++ b/src/util.js @@ -104,59 +104,6 @@ const EXE_NAME = { linux: "nw", }; -/** - * Replaces the ffmpeg file in the nwjs directory with the one provided - * - * @param {string} platform The platform to replace the ffmpeg file for - * @param {string} nwDir The directory of the nwjs installation - */ -const replaceFfmpeg = async (platform, nwDir) => { - let ffmpegFile; - if (platform === "linux") { - ffmpegFile = "libffmpeg.so"; - } else if (platform === "win") { - ffmpegFile = "ffmpeg.dll"; - } else if (platform === "osx") { - ffmpegFile = "libffmpeg.dylib"; - } - const src = path.resolve(nwDir, ffmpegFile); - if (platform === "linux") { - const dest = path.resolve(nwDir, "lib", ffmpegFile); - await fs.promises.copyFile(src, dest); - } else if (platform === "win") { - // don't do anything for windows because the extracted file is already in the correct path - // await copyFile(src, path.resolve(nwDir, ffmpegFile)); - } else if (platform === "osx") { - let dest = path.resolve( - nwDir, - "nwjs.app", - "Contents", - "Frameworks", - "nwjs Framework.framework", - "Versions", - "Current", - ffmpegFile, - ); - - try { - await fs.promises.copyFile(src, dest); - } catch (e) { - //some versions of node/macOS complain about destination being a file, and others complain when it is only a directory. - //the only thing I can think to do is to try both - dest = path.resolve( - nwDir, - "nwjs.app", - "Contents", - "Frameworks", - "nwjs Framework.framework", - "Versions", - "Current", - ); - await fs.promises.copyFile(src, dest); - } - } -}; - /** * Glob files * @@ -482,4 +429,4 @@ async function fileExists(filePath) { return exists; } -export default { fileExists, getReleaseInfo, getPath, PLATFORM_KV, ARCH_KV, EXE_NAME, replaceFfmpeg, globFiles, getNodeManifest, parse, validate }; +export default { fileExists, getReleaseInfo, getPath, PLATFORM_KV, ARCH_KV, EXE_NAME, globFiles, getNodeManifest, parse, validate }; diff --git a/test/fixture/demo.js b/test/fixture/demo.js index 1abc0101f..5355b123c 100644 --- a/test/fixture/demo.js +++ b/test/fixture/demo.js @@ -3,6 +3,5 @@ import nwbuild from "../../src/index.js"; await nwbuild({ mode: "get", flavor: "sdk", - platform: "osx", - srcDir: "app" + srcDir: "app", }); diff --git a/test/specs/bld.test.js b/test/specs/bld.test.js index 8de7999f8..d1a64bbfe 100644 --- a/test/specs/bld.test.js +++ b/test/specs/bld.test.js @@ -7,7 +7,7 @@ import chrome from "selenium-webdriver/chrome.js"; import { beforeAll, describe, it } from "vitest"; import build from "../../src/bld.js"; -import get from "../../src/get.js"; +import get from "../../src/get/index.js"; import util from "../../src/util.js"; const { Driver, ServiceBuilder, Options } = chrome; diff --git a/test/specs/get.test.js b/test/specs/get.test.js index 66428dd0b..f1ec05ab3 100644 --- a/test/specs/get.test.js +++ b/test/specs/get.test.js @@ -5,7 +5,7 @@ import process from "node:process"; import { beforeAll, describe, it } from "vitest"; -import get from '../../src/get.js'; +import get from '../../src/get/index.js'; describe("get", async () => { const nwOptions = {